Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented Trie Data Structure #162

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
* Disjointsets
* [Disjointset](./DataStructures/DisjointSets/DisjointSet.php)
* [Disjointsetnode](./DataStructures/DisjointSets/DisjointSetNode.php)
* Trie
* [Trie](./DataStructures/Trie/Trie.php)
* [TrieNode](./DataStructures/Trie/TrieNode.php)
* [Doublylinkedlist](./DataStructures/DoublyLinkedList.php)
* [Node](./DataStructures/Node.php)
* [Queue](./DataStructures/Queue.php)
Expand Down Expand Up @@ -115,6 +118,7 @@
* [Conversionstest](./tests/Conversions/ConversionsTest.php)
* Datastructures
* [Disjointsettest](./tests/DataStructures/DisjointSetTest.php)
* [Trie](./tests/DataStructures/TrieTest.php)
* [Doublylinkedlisttest](./tests/DataStructures/DoublyLinkedListTest.php)
* [Queuetest](./tests/DataStructures/QueueTest.php)
* [Singlylinkedlisttest](./tests/DataStructures/SinglyLinkedListTest.php)
Expand Down
163 changes: 163 additions & 0 deletions DataStructures/Trie/Trie.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

namespace DataStructures\Trie;

class Trie
{
private TrieNode $root;

public function __construct()
{
$this->root = new TrieNode();
}

/**
* Get the root node of the Trie.
*/
public function getRoot(): TrieNode
{
return $this->root;
}

/**
* Insert a word into the Trie.
*/
public function insert(string $word): void
{
$node = $this->root;
for ($i = 0; $i < strlen($word); $i++) {
$char = $word[$i];
$node = $node->addChild($char);
}
$node->isEndOfWord = true;
}

/**
* Search for a word in the Trie.
*/
public function search(string $word): bool
{
$node = $this->root;
for ($i = 0; $i < strlen($word); $i++) {
$char = $word[$i];
if (!$node->hasChild($char)) {
return false;
}
$node = $node->getChild($char);
}
return $node->isEndOfWord;
}

/**
* Find all words that start with a given prefix.
*/
public function startsWith(string $prefix): array
{
$node = $this->root;
for ($i = 0; $i < strlen($prefix); $i++) {
$char = $prefix[$i];
if (!$node->hasChild($char)) {
return [];
}
$node = $node->getChild($char);
}
return $this->findWordsFromNode($node, $prefix);
}

/**
* Helper function to find all words from a given node.
*/
private function findWordsFromNode(TrieNode $node, string $prefix): array
{
$words = [];
if ($node->isEndOfWord) {
$words[] = $prefix;
}

foreach ($node->children as $char => $childNode) {
$words = array_merge($words, $this->findWordsFromNode($childNode, $prefix . $char));
}

return $words;
}

/**
* Delete a word from the Trie.
* Recursively traverses the Trie and removes nodes
*
* @param string $word The word to delete.
* @return bool Returns true if the word was successfully deleted, otherwise false.
*/
public function delete(string $word): bool
{
return $this->deleteHelper($this->root, $word, 0);
}

/**
* Helper function for deleting a word.
* Recursively traverse the Trie and removes nodes.
*
* @param TrieNode $node The current node in the Trie.
* @param string $word The word being deleted.
* @param int $index The current index in the word.
* @return bool Returns true if the current node should be deleted, otherwise false.
*/
private function deleteHelper(TrieNode $node, string &$word, int $index): bool
{
if ($index === strlen($word)) {
if (!$node->isEndOfWord) {
return false;
}
$node->isEndOfWord = false;
return empty($node->children);
}

$char = $word[$index];
$childNode = $node->getChild($char);
if ($childNode === null) {
return false;
}

// Recursively delete the child node
$shouldDeleteCurrentNode = $this->deleteHelper($childNode, $word, $index + 1);

if ($shouldDeleteCurrentNode) {
unset($node->children[$char]);
return !$node->isEndOfWord; // true if current node is not the end of another word
}

return false;
}

/**
* Recursively traverses the Trie starting from the given node and collects all words.
*
* @param TrieNode $node The starting node for traversal.
* @param string $prefix The prefix of the current path in the Trie.
* @return array An array of words found in the Trie starting from the given node.
*/
public function traverseTrieNode(TrieNode $node, string $prefix = ''): array
{
$words = [];

if ($node->isEndOfWord) {
$words[] = $prefix;
}

foreach ($node->children as $char => $childNode) {
$words = array_merge($words, $this->traverseTrieNode($childNode, $prefix . $char));
}

return $words;
}

/**
* Gets all words stored in the Trie.
*
* @return array An array of all words in the Trie.
*/
public function getWords(): array
{
return $this->traverseTrieNode($this->root);
}
}
43 changes: 43 additions & 0 deletions DataStructures/Trie/TrieNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace DataStructures\Trie;

class TrieNode
{
/** @var array<string, TrieNode> */
public array $children;
public bool $isEndOfWord;

public function __construct()
{
$this->children = []; // Associative array where [ char => TrieNode ]
$this->isEndOfWord = false;
}

/**
* Add a child node for a character.
*/
public function addChild(string $char): TrieNode
{
if (!isset($this->children[$char])) {
$this->children[$char] = new TrieNode();
}
return $this->children[$char];
}

/**
* Check if a character has a child node.
*/
public function hasChild(string $char): bool
{
return isset($this->children[$char]);
}

/**
* Get the child node corresponding to a character.
*/
public function getChild(string $char): ?TrieNode
{
return $this->children[$char] ?? null;
}
}
137 changes: 137 additions & 0 deletions tests/DataStructures/TrieTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

namespace DataStructures;

require_once __DIR__ . '/../../DataStructures/Trie/Trie.php';
require_once __DIR__ . '/../../DataStructures/Trie/TrieNode.php';

use DataStructures\Trie\Trie;
use PHPUnit\Framework\TestCase;

class TrieTest extends TestCase
{
private Trie $trie;

protected function setUp(): void
{
$this->trie = new Trie();
}

public function testInsertAndSearch()
{
$this->trie->insert('the');
$this->trie->insert('universe');
$this->trie->insert('is');
$this->trie->insert('vast');

$this->assertTrue($this->trie->search('the'), 'Expected "the" to be found in the Trie.');
$this->assertTrue($this->trie->search('universe'), 'Expected "universe" to be found in the Trie.');
$this->assertTrue($this->trie->search('is'), 'Expected "is" to be found in the Trie.');
$this->assertTrue($this->trie->search('vast'), 'Expected "vast" to be found in the Trie.');
$this->assertFalse(
$this->trie->search('the universe'),
'Expected "the universe" not to be found in the Trie.'
);
}

public function testStartsWith()
{
$this->trie->insert('hello');
$this->assertEquals(['hello'], $this->trie->startsWith('he'), 'Expected words starting with "he" to be found.');
$this->assertEquals(
['hello'],
$this->trie->startsWith('hello'),
'Expected words starting with "hello" to be found.'
);
$this->assertEquals(
[],
$this->trie->startsWith('world'),
'Expected no words starting with "world" to be found.'
);
}

public function testDelete()
{
// Insert words into the Trie
$this->trie->insert('the');
$this->trie->insert('universe');
$this->trie->insert('is');
$this->trie->insert('vast');
$this->trie->insert('big');
$this->trie->insert('rather');

// Test deleting an existing word
$this->trie->delete('the');
$this->assertFalse($this->trie->search('the'), 'Expected "the" not to be found after deletion.');

// Test that other words are still present
$this->assertTrue($this->trie->search('universe'), 'Expected "universe" to be found.');
$this->assertTrue($this->trie->search('is'), 'Expected "is" to be found.');
$this->assertTrue($this->trie->search('vast'), 'Expected "vast" to be found.');
$this->assertTrue($this->trie->search('big'), 'Expected "big" to be found.');
$this->assertTrue($this->trie->search('rather'), 'Expected "rather" to be found.');
}

public function testDeleteNonExistentWord()
{
$this->trie->delete('nonexistent');
$this->assertFalse($this->trie->search('nonexistent'), 'Expected "nonexistent" to not be found.');
}

public function testTraverseTrieNode()
{
$this->trie->insert('hello');
$this->trie->insert('helium');
$this->trie->insert('helicopter');

$words = $this->trie->getWords();
$this->assertContains('hello', $words, 'Expected "hello" to be found in the Trie.');
$this->assertContains('helium', $words, 'Expected "helium" to be found in the Trie.');
$this->assertContains('helicopter', $words, 'Expected "helicopter" to be found in the Trie.');
$this->assertCount(3, $words, 'Expected 3 words in the Trie.');
}

public function testEmptyTrie()
{
$this->assertEquals([], $this->trie->getWords(), 'Expected an empty Trie to return an empty array.');
}

public function testGetWords()
{
$this->trie->insert('apple');
$this->trie->insert('app');
$this->trie->insert('applet');

$words = $this->trie->getWords();
$this->assertContains('apple', $words, 'Expected "apple" to be found in the Trie.');
$this->assertContains('app', $words, 'Expected "app" to be found in the Trie.');
$this->assertContains('applet', $words, 'Expected "applet" to be found in the Trie.');
$this->assertCount(3, $words, 'Expected 3 words in the Trie.');
}

public function testInsertEmptyString()
{
$this->trie->insert('');
$this->assertTrue($this->trie->search(''), 'Expected empty string to be found in the Trie.');
}

public function testDeleteEmptyString()
{
$this->trie->insert('');
$this->trie->delete('');
$this->assertFalse($this->trie->search(''), 'Expected empty string not to be found after deletion.');
}

public function testStartsWithWithCommonPrefix()
{
$this->trie->insert('trie');
$this->trie->insert('tried');
$this->trie->insert('trier');

$words = $this->trie->startsWith('tri');
$this->assertContains('trie', $words, 'Expected "trie" to be found with prefix "tri".');
$this->assertContains('tried', $words, 'Expected "tried" to be found with prefix "tri".');
$this->assertContains('trier', $words, 'Expected "trier" to be found with prefix "tri".');
$this->assertCount(3, $words, 'Expected 3 words with prefix "tri".');
}
}
Loading