From 081f0bd553b60bad366d927d3b4f6f35616b4186 Mon Sep 17 00:00:00 2001 From: Carlos MAtos Date: Tue, 26 Nov 2024 03:08:36 +0000 Subject: [PATCH] Set, HashSet and TreeSet added. --- README.md | 309 ++++++++++++++++++++++++- src/DisjointSet.php | 192 ++++++++++++++++ src/HashSet.php | 433 +++++++++++++++++++++++++++++++++++ src/Set.php | 256 +++++++++++++++++++++ src/TreeNode.php | 37 +++ src/TreeSet.php | 508 ++++++++++++++++++++++++++++++++++++++++++ tests/HashSetTest.php | 126 +++++++++++ tests/TreeSetTest.php | 144 ++++++++++++ 8 files changed, 2003 insertions(+), 2 deletions(-) create mode 100644 src/DisjointSet.php create mode 100644 src/HashSet.php create mode 100644 src/Set.php create mode 100644 src/TreeNode.php create mode 100644 src/TreeSet.php create mode 100644 tests/HashSetTest.php create mode 100644 tests/TreeSetTest.php diff --git a/README.md b/README.md index 3f1aa95..41ea6a7 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Daedalus is a powerful PHP library that provides advanced data structures and ut The library is designed with a focus on type safety, immutability, and event-driven architecture, making it an ideal choice for building robust and maintainable applications. +License: [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.en.html) + ## Why Use Daedalus? This library is particularly useful in scenarios where you need robust array handling with type safety and change tracking. Here are some real-world use cases: @@ -58,11 +60,12 @@ The library combines the power of PHP's ArrayObject with modern programming prac - PSR-11 compliant dependency injection container - Singleton pattern support - Automatic dependency resolution +- C# like Dictionary class ## Installation ```bash -composer require daedalus/enhanced-array-object +composer require cmatosbc/daedalus ``` ## Data Structures @@ -71,6 +74,33 @@ composer require daedalus/enhanced-array-object The `Dictionary` class provides a robust key-value collection with type safety and iteration capabilities. It implements `Iterator`, `Countable`, and `Serializable` interfaces, offering a comprehensive solution for managing key-value pairs. +#### Real-World Use Cases + +1. Configuration Management + - Store application settings with typed values + - Manage environment-specific configurations + - Handle feature flags and toggles + +2. HTTP Headers Management + - Store and manipulate HTTP headers + - Handle custom header mappings + - Normalize header names and values + +3. User Session Data + - Store user preferences + - Manage shopping cart items + - Cache user-specific settings + +4. Language Localization + - Map language keys to translations + - Handle multiple locale configurations + - Store message templates + +5. Cache Implementation + - Store computed results with unique keys + - Implement memory-efficient caching + - Manage cache invalidation + ```php use Daedalus\Dictionary; @@ -106,6 +136,281 @@ foreach ($dict as $key => $value) { $dict->clear(); ``` +### Set + +The `Set` class implements a collection of unique values with standard set operations. It provides methods for union, intersection, difference, and subset operations, making it ideal for mathematical set operations and managing unique collections. + +#### Real-World Use Cases + +1. User Permissions and Roles + - Store user capabilities + - Manage role assignments + - Calculate effective permissions through set operations + - Check required permission sets + +2. Social Network Connections + - Manage friend lists + - Find mutual friends (intersection) + - Suggest new connections (difference) + - Track group memberships + +3. Product Categories and Tags + - Manage product classifications + - Handle multiple category assignments + - Find products with common tags + - Calculate related products + +4. Event Management + - Track event attendees + - Manage waiting lists + - Find common participants between events + - Handle group registrations + +5. Data Deduplication + - Remove duplicate records + - Track unique visitors + - Manage email subscription lists + - Handle unique identifier collections + +```php +use Daedalus\Set; + +// Create new sets +$set1 = new Set([1, 2, 3]); +$set2 = new Set([2, 3, 4]); + +// Add and remove items +$set1->add(5); // Returns true (added) +$set1->add(1); // Returns false (already exists) +$set1->remove(1); // Returns true (removed) +$set1->remove(10); // Returns false (didn't exist) + +// Check for existence +$exists = $set1->contains(2); // Returns true + +// Set operations +$union = $set1->union($set2); // {2, 3, 4, 5} +$intersection = $set1->intersection($set2); // {2, 3} +$difference = $set1->difference($set2); // {5} + +// Check subset relationship +$isSubset = $set1->isSubsetOf($set2); // Returns false + +// Convert to array +$array = $set1->toArray(); // [2, 3, 5] + +// Iterate over set +foreach ($set1 as $item) { + echo $item . "\n"; +} + +// Clear all items +$set1->clear(); +``` + +The Set class features: +- Unique value storage +- Standard set operations (union, intersection, difference) +- Subset checking +- Object and scalar value support +- Iterator implementation for foreach loops +- Serialization support + +### DisjointSet + +The `DisjointSet` class extends `Set` to provide an efficient implementation of disjoint sets (union-find data structure) with path compression and union by rank optimizations. It's particularly useful for managing non-overlapping groups and determining connectivity between elements. + +#### Real-World Use Cases + +1. Social Network Analysis + - Track friend groups and communities + - Detect connected components in social graphs + - Analyze information spread patterns + - Identify isolated user clusters + +2. Network Infrastructure + - Monitor network connectivity + - Detect network partitions + - Manage redundant connections + - Track service dependencies + +3. Image Processing + - Connected component labeling + - Region segmentation + - Object detection + - Pixel clustering + +4. Game Development + - Track team/alliance memberships + - Manage territory control + - Handle resource ownership + - Implement faction systems + +5. Distributed Systems + - Partition management + - Cluster state tracking + - Service discovery + - Consensus group management + +```php +use Daedalus\DisjointSet; + +// Create a disjoint set +$ds = new DisjointSet(); + +// Create individual sets +$ds->makeSet("A"); +$ds->makeSet("B"); +$ds->makeSet("C"); +$ds->makeSet("D"); + +// Join sets together +$ds->union("A", "B"); // Now A and B are in the same set +$ds->union("C", "D"); // Now C and D are in the same set +$ds->union("B", "C"); // Now all elements are in the same set + +// Check if elements are connected +$connected = $ds->connected("A", "D"); // Returns true + +// Find the representative element of a set +$rep = $ds->find("B"); // Returns the set's representative + +// Get all elements in the same set +$set = $ds->getSet("A"); // Returns ["A", "B", "C", "D"] + +// Count number of disjoint sets +$count = $ds->countSets(); // Returns 1 + +// Clear all sets +$ds->clear(); +``` + +The DisjointSet class features: +- Efficient union and find operations (O(α(n)), where α is the inverse Ackermann function) +- Path compression optimization +- Union by rank optimization +- Set connectivity checking +- Set membership queries +- Multiple set management + +### HashSet + +The `HashSet` class extends `Set` to provide constant-time performance for basic operations. It uses hash-based storage with no guarantee of iteration order, similar to Java's HashSet. + +#### Real-World Use Cases + +1. Caching Systems + - Store cache keys + - Track recently accessed items + - Manage unique identifiers + - Handle session tokens + +2. Data Validation + - Track processed records + - Validate unique entries + - Filter duplicate submissions + - Check for existing values + +3. Analytics and Tracking + - Track unique visitors + - Monitor unique events + - Store distinct metrics + - Log unique errors + +```php +use Daedalus\HashSet; + +// Create a new HashSet with custom load factor and capacity +$set = new HashSet([], 0.75, 16); + +// Add elements +$set->add("apple"); +$set->add("banana"); +$set->add("apple"); // Returns false (already exists) + +// Bulk operations with another set +$otherSet = new HashSet(["banana", "cherry"]); +$set->addAll($otherSet); // Add all elements from otherSet +$set->removeAll($otherSet); // Remove all elements that exist in otherSet +$set->retainAll($otherSet); // Keep only elements that exist in otherSet + +// Check contents +$exists = $set->contains("apple"); // Returns true +$count = $set->count(); // Get number of elements + +// Convert to array (no guaranteed order) +$array = $set->toArray(); + +// Clear the set +$set->clear(); +``` + +### TreeSet + +The `TreeSet` class implements a self-balancing binary search tree (AVL tree) that maintains elements in sorted order, similar to Java's TreeSet. + +#### Real-World Use Cases + +1. Ranking Systems + - Leaderboard management + - Score tracking + - Priority queues + - Tournament rankings + +2. Time-based Operations + - Event scheduling + - Task prioritization + - Deadline management + - Log entry ordering + +3. Range Queries + - Price range searches + - Date range filtering + - Numeric range queries + - Version management + +```php +use Daedalus\TreeSet; + +// Create a new TreeSet +$set = new TreeSet([3, 1, 4, 1, 5]); // Duplicates are automatically removed + +// Add elements (maintains order) +$set->add(2); +$set->add(6); + +// Access ordered elements +$first = $set->first(); // Get smallest element (1) +$last = $set->last(); // Get largest element (6) + +// Find adjacent elements +$lower = $set->lower(4); // Get largest element < 4 (3) +$higher = $set->higher(4); // Get smallest element > 4 (5) + +// Check contents +$exists = $set->contains(3); // Returns true +$count = $set->count(); // Get number of elements + +// Convert to sorted array +$array = $set->toArray(); // [1, 2, 3, 4, 5, 6] + +// Iterate in order +foreach ($set as $element) { + echo $element . "\n"; +} + +// Clear the set +$set->clear(); +``` + +The TreeSet class features: +- Ordered element storage +- Logarithmic-time operations (O(log n)) +- Efficient range operations +- Natural ordering for scalar types +- Custom ordering via __toString for objects +- AVL tree self-balancing + ### Enhanced Array Object A PHP library that provides an enhanced version of PHP's ArrayObject with additional features like type safety, event handling, immutability options, and a PSR-11 compliant dependency injection container. @@ -351,4 +656,4 @@ Contributions are welcome! Please feel free to submit a Pull Request. ## License -This library is licensed under the MIT License - see the LICENSE file for details. +This library is licensed under the GNU General Public License v3.0 - see the LICENSE file for details. diff --git a/src/DisjointSet.php b/src/DisjointSet.php new file mode 100644 index 0000000..dd0d516 --- /dev/null +++ b/src/DisjointSet.php @@ -0,0 +1,192 @@ +makeSet($item); + } + } + + /** + * Creates a new set containing only the specified item + * + * @param mixed $item The item to create a set for + * @return bool True if the set was created, false if item already exists + */ + public function makeSet(mixed $item): bool + { + $hash = $this->hash($item); + if (isset($this->parent[$hash])) { + return false; + } + + if (parent::add($item)) { + $this->parent[$hash] = $hash; + $this->rank[$hash] = 0; + return true; + } + + return false; + } + + /** + * Finds the representative element of the set containing the item + * + * Implements path compression: all nodes along the path to the root + * are made to point directly to the root. + * + * @param mixed $item The item to find the representative for + * @return mixed|null The representative item, or null if item doesn't exist + */ + public function find(mixed $item): mixed + { + $hash = $this->hash($item); + if (!isset($this->parent[$hash])) { + return null; + } + + // Path compression: make each node point directly to the root + if ($this->parent[$hash] !== $hash) { + $this->parent[$hash] = $this->hash($this->find($this->getItemByHash($this->parent[$hash]))); + } + + return $this->getItemByHash($this->parent[$hash]); + } + + /** + * Merges the sets containing the two items + * + * Implements union by rank: the root with higher rank becomes the parent. + * If ranks are equal, the second root becomes parent and its rank increases. + * + * @param mixed $item1 First item + * @param mixed $item2 Second item + * @return bool True if sets were merged, false if items don't exist or are already in same set + */ + public function union(mixed $item1, mixed $item2): bool + { + $root1 = $this->hash($this->find($item1)); + $root2 = $this->hash($this->find($item2)); + + if ($root1 === null || $root2 === null || $root1 === $root2) { + return false; + } + + // Union by rank + if ($this->rank[$root1] > $this->rank[$root2]) { + $this->parent[$root2] = $root1; + } else { + $this->parent[$root1] = $root2; + if ($this->rank[$root1] === $this->rank[$root2]) { + $this->rank[$root2]++; + } + } + + return true; + } + + /** + * Checks if two items are in the same set + * + * @param mixed $item1 First item + * @param mixed $item2 Second item + * @return bool True if items are in the same set, false otherwise + */ + public function connected(mixed $item1, mixed $item2): bool + { + $root1 = $this->find($item1); + $root2 = $this->find($item2); + + return $root1 !== null && $root1 === $root2; + } + + /** + * Gets all items in the same set as the given item + * + * @param mixed $item The item to get the set for + * @return array Array of items in the same set + */ + public function getSet(mixed $item): array + { + $result = []; + $root = $this->find($item); + + if ($root === null) { + return $result; + } + + foreach ($this->toArray() as $element) { + if ($this->find($element) === $root) { + $result[] = $element; + } + } + + return $result; + } + + /** + * Gets the number of disjoint sets + * + * @return int Number of disjoint sets + */ + public function countSets(): int + { + $count = 0; + foreach ($this->parent as $hash => $parent) { + if ($hash === $parent) { + $count++; + } + } + return $count; + } + + /** + * Clears all sets + */ + public function clear(): void + { + parent::clear(); + $this->parent = []; + $this->rank = []; + } + + /** + * Gets an item by its hash + * + * @param string $hash The hash to look up + * @return mixed The item with the given hash + */ + private function getItemByHash(string $hash): mixed + { + foreach ($this->toArray() as $item) { + if ($this->hash($item) === $hash) { + return $item; + } + } + return null; + } +} diff --git a/src/HashSet.php b/src/HashSet.php new file mode 100644 index 0000000..2cd1f42 --- /dev/null +++ b/src/HashSet.php @@ -0,0 +1,433 @@ +loadFactor = $loadFactor; + $this->capacity = $initialCapacity; + $this->initializeBuckets(); + + foreach ($items as $item) { + $this->add($item); + } + } + + /** + * Adds an item to the set + * + * @param mixed $item The item to add + * @return bool True if the item was added, false if it already existed + */ + public function add(mixed $item): bool + { + if ($this->shouldRehash()) { + $this->rehash(); + } + + $hash = $this->hash($item); + $bucket = $this->getBucketIndex($hash); + + if (!isset($this->buckets[$bucket])) { + $this->buckets[$bucket] = []; + } + + foreach ($this->buckets[$bucket] as $existingItem) { + if ($this->equals($existingItem, $item)) { + return false; + } + } + + $this->buckets[$bucket][] = $item; + $this->size++; + return true; + } + + /** + * Removes an item from the set + * + * @param mixed $item The item to remove + * @return bool True if the item was removed, false if it didn't exist + */ + public function remove(mixed $item): bool + { + $hash = $this->hash($item); + $bucket = $this->getBucketIndex($hash); + + if (!isset($this->buckets[$bucket])) { + return false; + } + + foreach ($this->buckets[$bucket] as $key => $existingItem) { + if ($this->equals($existingItem, $item)) { + unset($this->buckets[$bucket][$key]); + $this->size--; + if (empty($this->buckets[$bucket])) { + unset($this->buckets[$bucket]); + } else { + $this->buckets[$bucket] = array_values($this->buckets[$bucket]); + } + return true; + } + } + + return false; + } + + /** + * Checks if an item exists in the set + * + * @param mixed $item The item to check + * @return bool True if the item exists, false otherwise + */ + public function contains(mixed $item): bool + { + $hash = $this->hash($item); + $bucket = $this->getBucketIndex($hash); + + if (!isset($this->buckets[$bucket])) { + return false; + } + + foreach ($this->buckets[$bucket] as $existingItem) { + if ($this->equals($existingItem, $item)) { + return true; + } + } + + return false; + } + + /** + * Adds all items from another set to this set + * + * @param HashSet $other The other set + * @return bool True if any items were added + */ + public function addAll(HashSet $other): bool + { + $modified = false; + foreach ($other->toArray() as $item) { + if ($this->add($item)) { + $modified = true; + } + } + return $modified; + } + + /** + * Removes all items that exist in another set + * + * @param HashSet $other The other set + * @return bool True if any items were removed + */ + public function removeAll(HashSet $other): bool + { + $modified = false; + foreach ($other->toArray() as $item) { + if ($this->remove($item)) { + $modified = true; + } + } + return $modified; + } + + /** + * Retains only items that exist in another set + * + * @param HashSet $other The other set + * @return bool True if any items were removed + */ + public function retainAll(HashSet $other): bool + { + $modified = false; + $toRemove = []; + + foreach ($this->toArray() as $item) { + if (!$other->contains($item)) { + $toRemove[] = $item; + $modified = true; + } + } + + foreach ($toRemove as $item) { + $this->remove($item); + } + + return $modified; + } + + /** + * Converts the set to an array + * + * @return array Array containing all items + */ + public function toArray(): array + { + $result = []; + foreach ($this->buckets as $bucket) { + foreach ($bucket as $item) { + $result[] = $item; + } + } + return $result; + } + + /** + * Clears all items from the set + */ + public function clear(): void + { + $this->buckets = []; + $this->size = 0; + $this->initializeBuckets(); + } + + /** + * Gets the current value during iteration + * + * @return mixed The current value + */ + public function current(): mixed + { + $bucket = $this->findNextNonEmptyBucket(); + return $bucket === null ? null : $bucket[$this->positionInBucket]; + } + + /** + * Gets the current key during iteration + * + * @return int The current position + */ + public function key(): int + { + return $this->currentBucket * $this->capacity + $this->positionInBucket; + } + + /** + * Moves to the next position in iteration + */ + public function next(): void + { + $bucket = $this->findNextNonEmptyBucket(); + if ($bucket === null) { + return; + } + + $this->positionInBucket++; + if ($this->positionInBucket >= count($bucket)) { + $this->currentBucket++; + $this->positionInBucket = 0; + } + } + + /** + * Rewinds the iterator to the beginning + */ + public function rewind(): void + { + $this->currentBucket = 0; + $this->positionInBucket = 0; + } + + /** + * Checks if the current position is valid + * + * @return bool True if the position is valid + */ + public function valid(): bool + { + return $this->findNextNonEmptyBucket() !== null; + } + + /** + * Gets the count of items in the set + * + * @return int The number of items + */ + public function count(): int + { + return $this->size; + } + + /** + * Serializes the set to a string + * + * @return string The serialized set + */ + public function serialize(): string + { + return serialize([ + 'buckets' => $this->buckets, + 'loadFactor' => $this->loadFactor, + 'capacity' => $this->capacity, + 'size' => $this->size + ]); + } + + /** + * Unserializes a string back into a set + * + * @param string $data The serialized set data + */ + public function unserialize(string $data): void + { + $data = unserialize($data); + $this->buckets = $data['buckets']; + $this->loadFactor = $data['loadFactor']; + $this->capacity = $data['capacity']; + $this->size = $data['size']; + } + + /** + * Gets the load factor + * + * @return float Current load factor + */ + public function getLoadFactor(): float + { + return $this->loadFactor; + } + + /** + * Gets the current capacity + * + * @return int Current capacity + */ + public function getCapacity(): int + { + return $this->capacity; + } + + /** + * Initializes the bucket array + */ + private function initializeBuckets(): void + { + $this->buckets = array_fill(0, $this->capacity, []); + } + + /** + * Checks if rehashing is needed + * + * @return bool True if rehashing is needed + */ + private function shouldRehash(): bool + { + return $this->size >= $this->capacity * $this->loadFactor; + } + + /** + * Rehashes the set with double capacity + */ + private function rehash(): void + { + $oldBuckets = $this->buckets; + $this->capacity *= 2; + $this->initializeBuckets(); + $this->size = 0; + + foreach ($oldBuckets as $bucket) { + foreach ($bucket as $item) { + $this->add($item); + } + } + } + + /** + * Gets the bucket index for a hash + * + * @param string $hash The hash value + * @return int Bucket index + */ + private function getBucketIndex(string $hash): int + { + return hexdec(substr($hash, 0, 8)) % $this->capacity; + } + + /** + * Generates a hash for an item + * + * @param mixed $item The item to hash + * @return string The hash value + */ + private function hash(mixed $item): string + { + if (is_object($item)) { + return spl_object_hash($item); + } + return md5(serialize($item)); + } + + /** + * Compares two items for equality + * + * @param mixed $a First item + * @param mixed $b Second item + * @return bool True if items are equal + */ + private function equals(mixed $a, mixed $b): bool + { + if (is_object($a) && is_object($b)) { + return spl_object_hash($a) === spl_object_hash($b); + } + return $a === $b; + } + + /** + * Finds the next non-empty bucket for iteration + * + * @return array|null The bucket or null if none found + */ + private function findNextNonEmptyBucket(): ?array + { + while ($this->currentBucket < $this->capacity) { + if (isset($this->buckets[$this->currentBucket]) && + !empty($this->buckets[$this->currentBucket]) && + $this->positionInBucket < count($this->buckets[$this->currentBucket])) { + return $this->buckets[$this->currentBucket]; + } + $this->currentBucket++; + $this->positionInBucket = 0; + } + return null; + } +} diff --git a/src/Set.php b/src/Set.php new file mode 100644 index 0000000..1770e15 --- /dev/null +++ b/src/Set.php @@ -0,0 +1,256 @@ +add($item); + } + } + + /** + * Adds an item to the set + * + * @param mixed $item The item to add + * @return bool True if the item was added, false if it already existed + */ + public function add(mixed $item): bool + { + $hash = $this->hash($item); + if (!isset($this->items[$hash])) { + $this->items[$hash] = $item; + return true; + } + return false; + } + + /** + * Removes an item from the set + * + * @param mixed $item The item to remove + * @return bool True if the item was removed, false if it didn't exist + */ + public function remove(mixed $item): bool + { + $hash = $this->hash($item); + if (isset($this->items[$hash])) { + unset($this->items[$hash]); + return true; + } + return false; + } + + /** + * Checks if an item exists in the set + * + * @param mixed $item The item to check + * @return bool True if the item exists, false otherwise + */ + public function contains(mixed $item): bool + { + return isset($this->items[$this->hash($item)]); + } + + /** + * Returns the union of this set with another set + * + * @param Set $other The other set + * @return Set A new set containing all items from both sets + */ + public function union(Set $other): Set + { + $result = new Set(); + foreach ($this->items as $item) { + $result->add($item); + } + foreach ($other->toArray() as $item) { + $result->add($item); + } + return $result; + } + + /** + * Returns the intersection of this set with another set + * + * @param Set $other The other set + * @return Set A new set containing items present in both sets + */ + public function intersection(Set $other): Set + { + $result = new Set(); + foreach ($this->items as $item) { + if ($other->contains($item)) { + $result->add($item); + } + } + return $result; + } + + /** + * Returns the difference of this set with another set + * + * @param Set $other The other set + * @return Set A new set containing items present in this set but not in the other + */ + public function difference(Set $other): Set + { + $result = new Set(); + foreach ($this->items as $item) { + if (!$other->contains($item)) { + $result->add($item); + } + } + return $result; + } + + /** + * Checks if this set is a subset of another set + * + * @param Set $other The other set + * @return bool True if this set is a subset of the other set + */ + public function isSubsetOf(Set $other): bool + { + foreach ($this->items as $item) { + if (!$other->contains($item)) { + return false; + } + } + return true; + } + + /** + * Converts the set to an array + * + * @return array Array containing all items in the set + */ + public function toArray(): array + { + return array_values($this->items); + } + + /** + * Clears all items from the set + */ + public function clear(): void + { + $this->items = []; + } + + /** + * Gets the current value during iteration + * + * @return mixed The current value + */ + public function current(): mixed + { + return array_values($this->items)[$this->position]; + } + + /** + * Gets the current key during iteration + * + * @return int The current position + */ + public function key(): int + { + return $this->position; + } + + /** + * Moves to the next position in iteration + */ + public function next(): void + { + ++$this->position; + } + + /** + * Rewinds the iterator to the beginning + */ + public function rewind(): void + { + $this->position = 0; + } + + /** + * Checks if the current position is valid + * + * @return bool True if the position is valid, false otherwise + */ + public function valid(): bool + { + return isset(array_values($this->items)[$this->position]); + } + + /** + * Gets the count of items in the set + * + * @return int The number of items + */ + public function count(): int + { + return count($this->items); + } + + /** + * Serializes the set to a string + * + * @return string The serialized set + */ + public function serialize(): string + { + return serialize($this->items); + } + + /** + * Unserializes a string back into a set + * + * @param string $data The serialized set data + */ + public function unserialize(string $data): void + { + $this->items = unserialize($data); + } + + /** + * Generates a hash for an item + * + * @param mixed $item The item to hash + * @return string The hash value + */ + private function hash(mixed $item): string + { + if (is_object($item)) { + return spl_object_hash($item); + } + return md5(serialize($item)); + } +} diff --git a/src/TreeNode.php b/src/TreeNode.php new file mode 100644 index 0000000..2c97f20 --- /dev/null +++ b/src/TreeNode.php @@ -0,0 +1,37 @@ +value = $value; + } +} diff --git a/src/TreeSet.php b/src/TreeSet.php new file mode 100644 index 0000000..90a313d --- /dev/null +++ b/src/TreeSet.php @@ -0,0 +1,508 @@ +add($item); + } + } + + /** + * Adds an item to the set + * + * @param mixed $item The item to add + * @return bool True if the item was added, false if it already existed + */ + public function add(mixed $item): bool + { + if ($this->contains($item)) { + return false; + } + + $this->root = $this->insertNode($this->root, $item); + $this->size++; + $this->refreshIterationCache(); + return true; + } + + /** + * Removes an item from the set + * + * @param mixed $item The item to remove + * @return bool True if the item was removed, false if it didn't exist + */ + public function remove(mixed $item): bool + { + if (!$this->contains($item)) { + return false; + } + + $this->root = $this->removeNode($this->root, $item); + $this->size--; + $this->refreshIterationCache(); + return true; + } + + /** + * Checks if an item exists in the set + * + * @param mixed $item The item to check + * @return bool True if the item exists, false otherwise + */ + public function contains(mixed $item): bool + { + return $this->findNode($this->root, $item) !== null; + } + + /** + * Gets the first (smallest) item in the set + * + * @return mixed|null The first item, or null if set is empty + */ + public function first(): mixed + { + if ($this->root === null) { + return null; + } + + $node = $this->root; + while ($node->left !== null) { + $node = $node->left; + } + return $node->value; + } + + /** + * Gets the last (largest) item in the set + * + * @return mixed|null The last item, or null if set is empty + */ + public function last(): mixed + { + if ($this->root === null) { + return null; + } + + $node = $this->root; + while ($node->right !== null) { + $node = $node->right; + } + return $node->value; + } + + /** + * Gets the item less than the given item + * + * @param mixed $item The reference item + * @return mixed|null The lower item, or null if none exists + */ + public function lower(mixed $item): mixed + { + $result = null; + $node = $this->root; + + while ($node !== null) { + $cmp = $this->compare($item, $node->value); + if ($cmp <= 0) { + $node = $node->left; + } else { + $result = $node->value; + $node = $node->right; + } + } + + return $result; + } + + /** + * Gets the item greater than the given item + * + * @param mixed $item The reference item + * @return mixed|null The higher item, or null if none exists + */ + public function higher(mixed $item): mixed + { + $result = null; + $node = $this->root; + + while ($node !== null) { + $cmp = $this->compare($item, $node->value); + if ($cmp >= 0) { + $node = $node->right; + } else { + $result = $node->value; + $node = $node->left; + } + } + + return $result; + } + + /** + * Converts the set to an array + * + * @return array Sorted array of all items + */ + public function toArray(): array + { + return $this->iterationCache; + } + + /** + * Clears all items from the set + */ + public function clear(): void + { + $this->root = null; + $this->size = 0; + $this->refreshIterationCache(); + } + + /** + * Gets the current value during iteration + * + * @return mixed The current value + */ + public function current(): mixed + { + return $this->iterationCache[$this->position]; + } + + /** + * Gets the current key during iteration + * + * @return int The current position + */ + public function key(): int + { + return $this->position; + } + + /** + * Moves to the next position in iteration + */ + public function next(): void + { + $this->position++; + } + + /** + * Rewinds the iterator to the beginning + */ + public function rewind(): void + { + $this->position = 0; + } + + /** + * Checks if the current position is valid + * + * @return bool True if the position is valid, false otherwise + */ + public function valid(): bool + { + return isset($this->iterationCache[$this->position]); + } + + /** + * Gets the count of items in the set + * + * @return int The number of items + */ + public function count(): int + { + return $this->size; + } + + /** + * Serializes the set to a string + * + * @return string The serialized set + */ + public function serialize(): string + { + return serialize([ + 'items' => $this->toArray() + ]); + } + + /** + * Unserializes a string back into a set + * + * @param string $data The serialized set data + */ + public function unserialize(string $data): void + { + $data = unserialize($data); + $this->clear(); + foreach ($data['items'] as $item) { + $this->add($item); + } + } + + /** + * Compares two items + * + * @param mixed $a First item + * @param mixed $b Second item + * @return int Negative if a < b, 0 if equal, positive if a > b + */ + private function compare(mixed $a, mixed $b): int + { + if (is_object($a) && is_object($b)) { + return strcmp((string)$a, (string)$b); + } + return $a <=> $b; + } + + /** + * Inserts a node into the tree + * + * @param TreeNode|null $node Current node + * @param mixed $item Item to insert + * @return TreeNode New or updated node + */ + private function insertNode(?TreeNode $node, mixed $item): TreeNode + { + if ($node === null) { + return new TreeNode($item); + } + + $cmp = $this->compare($item, $node->value); + if ($cmp < 0) { + $node->left = $this->insertNode($node->left, $item); + } elseif ($cmp > 0) { + $node->right = $this->insertNode($node->right, $item); + } + + return $this->balance($node); + } + + /** + * Removes a node from the tree + * + * @param TreeNode|null $node Current node + * @param mixed $item Item to remove + * @return TreeNode|null New or updated node + */ + private function removeNode(?TreeNode $node, mixed $item): ?TreeNode + { + if ($node === null) { + return null; + } + + $cmp = $this->compare($item, $node->value); + if ($cmp < 0) { + $node->left = $this->removeNode($node->left, $item); + } elseif ($cmp > 0) { + $node->right = $this->removeNode($node->right, $item); + } else { + if ($node->left === null) { + return $node->right; + } elseif ($node->right === null) { + return $node->left; + } + + $successor = $this->findMin($node->right); + $node->value = $successor->value; + $node->right = $this->removeNode($node->right, $successor->value); + } + + return $this->balance($node); + } + + /** + * Finds a node in the tree + * + * @param TreeNode|null $node Current node + * @param mixed $item Item to find + * @return TreeNode|null Found node or null + */ + private function findNode(?TreeNode $node, mixed $item): ?TreeNode + { + if ($node === null) { + return null; + } + + $cmp = $this->compare($item, $node->value); + if ($cmp < 0) { + return $this->findNode($node->left, $item); + } elseif ($cmp > 0) { + return $this->findNode($node->right, $item); + } + return $node; + } + + /** + * Finds the minimum node in a subtree + * + * @param TreeNode $node Root of subtree + * @return TreeNode Minimum node + */ + private function findMin(TreeNode $node): TreeNode + { + while ($node->left !== null) { + $node = $node->left; + } + return $node; + } + + /** + * Gets the height of a node + * + * @param TreeNode|null $node The node + * @return int Height of the node + */ + private function height(?TreeNode $node): int + { + return $node === null ? 0 : $node->height; + } + + /** + * Updates the height of a node + * + * @param TreeNode $node The node to update + */ + private function updateHeight(TreeNode $node): void + { + $node->height = max($this->height($node->left), $this->height($node->right)) + 1; + } + + /** + * Gets the balance factor of a node + * + * @param TreeNode $node The node + * @return int Balance factor + */ + private function getBalance(TreeNode $node): int + { + return $this->height($node->left) - $this->height($node->right); + } + + /** + * Performs a right rotation + * + * @param TreeNode $y Root node + * @return TreeNode New root after rotation + */ + private function rotateRight(TreeNode $y): TreeNode + { + $x = $y->left; + $T2 = $x->right; + + $x->right = $y; + $y->left = $T2; + + $this->updateHeight($y); + $this->updateHeight($x); + + return $x; + } + + /** + * Performs a left rotation + * + * @param TreeNode $x Root node + * @return TreeNode New root after rotation + */ + private function rotateLeft(TreeNode $x): TreeNode + { + $y = $x->right; + $T2 = $y->left; + + $y->left = $x; + $x->right = $T2; + + $this->updateHeight($x); + $this->updateHeight($y); + + return $y; + } + + /** + * Balances a node + * + * @param TreeNode $node Node to balance + * @return TreeNode Balanced node + */ + private function balance(TreeNode $node): TreeNode + { + $this->updateHeight($node); + $balance = $this->getBalance($node); + + // Left Heavy + if ($balance > 1) { + if ($this->getBalance($node->left) < 0) { + $node->left = $this->rotateLeft($node->left); + } + return $this->rotateRight($node); + } + + // Right Heavy + if ($balance < -1) { + if ($this->getBalance($node->right) > 0) { + $node->right = $this->rotateRight($node->right); + } + return $this->rotateLeft($node); + } + + return $node; + } + + /** + * Refreshes the iteration cache + */ + private function refreshIterationCache(): void + { + $this->iterationCache = []; + $this->inorderTraversal($this->root); + } + + /** + * Performs an inorder traversal of the tree + * + * @param TreeNode|null $node Current node + */ + private function inorderTraversal(?TreeNode $node): void + { + if ($node !== null) { + $this->inorderTraversal($node->left); + $this->iterationCache[] = $node->value; + $this->inorderTraversal($node->right); + } + } +} diff --git a/tests/HashSetTest.php b/tests/HashSetTest.php new file mode 100644 index 0000000..d9f0ebb --- /dev/null +++ b/tests/HashSetTest.php @@ -0,0 +1,126 @@ +set = new HashSet(); + } + + public function testAddAndContains(): void + { + $this->assertTrue($this->set->add("apple")); + $this->assertTrue($this->set->contains("apple")); + $this->assertFalse($this->set->add("apple")); // Adding duplicate + $this->assertEquals(1, $this->set->count()); + } + + public function testRemove(): void + { + $this->set->add("apple"); + $this->set->add("banana"); + + $this->assertTrue($this->set->remove("apple")); + $this->assertFalse($this->set->contains("apple")); + $this->assertEquals(1, $this->set->count()); + + $this->assertFalse($this->set->remove("nonexistent")); + } + + public function testClear(): void + { + $this->set->add("apple"); + $this->set->add("banana"); + + $this->set->clear(); + $this->assertEquals(0, $this->set->count()); + $this->assertFalse($this->set->contains("apple")); + } + + public function testBulkOperations(): void + { + $this->set->add("apple"); + $this->set->add("banana"); + + $otherSet = new HashSet(["banana", "cherry"]); + + // Test addAll + $this->assertTrue($this->set->addAll($otherSet)); + $this->assertEquals(3, $this->set->count()); + $this->assertTrue($this->set->contains("cherry")); + + // Test removeAll + $this->assertTrue($this->set->removeAll($otherSet)); + $this->assertEquals(1, $this->set->count()); + $this->assertTrue($this->set->contains("apple")); + + // Test retainAll + $this->set->add("banana"); + $this->assertTrue($this->set->retainAll($otherSet)); + $this->assertEquals(1, $this->set->count()); + $this->assertTrue($this->set->contains("banana")); + } + + public function testCustomLoadFactorAndCapacity(): void + { + $customSet = new HashSet([], 0.75, 32); + + for ($i = 0; $i < 24; $i++) { // Fill up to load factor + $customSet->add("item$i"); + } + + $this->assertEquals(24, $customSet->count()); + $this->assertTrue($customSet->contains("item0")); + $this->assertTrue($customSet->contains("item23")); + } + + public function testIterator(): void + { + $items = ["apple", "banana", "cherry"]; + foreach ($items as $item) { + $this->set->add($item); + } + + $collectedItems = []; + foreach ($this->set as $item) { + $collectedItems[] = $item; + } + + sort($collectedItems); // Sort since HashSet doesn't guarantee order + $this->assertEquals($items, $collectedItems); + } + + public function testToArray(): void + { + $items = ["apple", "banana", "cherry"]; + foreach ($items as $item) { + $this->set->add($item); + } + + $array = $this->set->toArray(); + sort($array); // Sort since HashSet doesn't guarantee order + $this->assertEquals($items, $array); + } + + public function testObjectStorage(): void + { + $obj1 = new \stdClass(); + $obj1->id = 1; + $obj2 = new \stdClass(); + $obj2->id = 2; + + $this->set->add($obj1); + $this->set->add($obj2); + + $this->assertEquals(2, $this->set->count()); + $this->assertTrue($this->set->contains($obj1)); + $this->assertTrue($this->set->contains($obj2)); + } +} diff --git a/tests/TreeSetTest.php b/tests/TreeSetTest.php new file mode 100644 index 0000000..617d181 --- /dev/null +++ b/tests/TreeSetTest.php @@ -0,0 +1,144 @@ +set = new TreeSet(); + } + + public function testAddAndContains(): void + { + $this->assertTrue($this->set->add(5)); + $this->assertTrue($this->set->contains(5)); + $this->assertFalse($this->set->add(5)); // Adding duplicate + $this->assertEquals(1, $this->set->count()); + } + + public function testOrderedInsertion(): void + { + $numbers = [5, 3, 7, 1, 9, 2, 8, 4, 6]; + foreach ($numbers as $number) { + $this->set->add($number); + } + + $expected = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + $this->assertEquals($expected, $this->set->toArray()); + } + + public function testFirstAndLast(): void + { + $numbers = [5, 3, 7, 1, 9]; + foreach ($numbers as $number) { + $this->set->add($number); + } + + $this->assertEquals(1, $this->set->first()); + $this->assertEquals(9, $this->set->last()); + } + + public function testLowerAndHigher(): void + { + $numbers = [5, 3, 7, 1, 9]; + foreach ($numbers as $number) { + $this->set->add($number); + } + + $this->assertEquals(3, $this->set->lower(5)); + $this->assertEquals(7, $this->set->higher(5)); + $this->assertEquals(null, $this->set->lower(1)); + $this->assertEquals(null, $this->set->higher(9)); + } + + public function testRemove(): void + { + $numbers = [5, 3, 7]; + foreach ($numbers as $number) { + $this->set->add($number); + } + + $this->assertTrue($this->set->remove(3)); + $this->assertEquals([5, 7], $this->set->toArray()); + $this->assertFalse($this->set->remove(3)); // Already removed + } + + public function testClear(): void + { + $numbers = [5, 3, 7]; + foreach ($numbers as $number) { + $this->set->add($number); + } + + $this->set->clear(); + $this->assertEquals(0, $this->set->count()); + $this->assertNull($this->set->first()); + $this->assertNull($this->set->last()); + } + + public function testIterator(): void + { + $numbers = [5, 3, 7, 1, 9]; + foreach ($numbers as $number) { + $this->set->add($number); + } + + $result = []; + foreach ($this->set as $number) { + $result[] = $number; + } + + $this->assertEquals([1, 3, 5, 7, 9], $result); + } + + public function testObjectOrdering(): void + { + $obj1 = new class { + public function __toString() { return "1"; } + }; + $obj2 = new class { + public function __toString() { return "2"; } + }; + $obj3 = new class { + public function __toString() { return "3"; } + }; + + $this->set->add($obj2); + $this->set->add($obj1); + $this->set->add($obj3); + + $result = array_map(fn($obj) => (string)$obj, $this->set->toArray()); + $this->assertEquals(["1", "2", "3"], $result); + } + + public function testEmptySetOperations(): void + { + $this->assertNull($this->set->first()); + $this->assertNull($this->set->last()); + $this->assertNull($this->set->lower(5)); + $this->assertNull($this->set->higher(5)); + $this->assertEquals(0, $this->set->count()); + $this->assertEquals([], $this->set->toArray()); + } + + public function testBalancing(): void + { + // Test AVL tree balancing with sequential insertions + for ($i = 1; $i <= 7; $i++) { + $this->set->add($i); + } + + // Verify order is maintained after balancing + $this->assertEquals(range(1, 7), $this->set->toArray()); + + // Test removal maintains balance + $this->set->remove(4); + $this->assertEquals([1, 2, 3, 5, 6, 7], $this->set->toArray()); + } +}