cozylife/hackfastalgos

View on GitHub
lib/datastructure/ternarysearchtrie.php

Summary

Maintainability
A
0 mins
Test Coverage
<?HH
/**
 * Hack Fast Algos
 *
 * Implementation of a Ternary Search Tree (TST)
 * Learn more:
 * @link https://en.wikipedia.org/wiki/Ternary_search_tree
 * @link http://algs4.cs.princeton.edu/52trie/TST.java.html
 */

namespace HackFastAlgos\DataStructure;

class TernarySearchTrie
{
    private ?TreeNode $root = null;
    private int $longestPrefixOffset = 0;

    public function get(string $string) : ?int
    {
        if (strlen($string) === 0 || $this->root === null) {
            return null;
        }

        $node = $this->getLastNodeForString($this->root, $string);
        return $node === null ? null : $node->value;
    }

    public function put(string $string, ?int $value)
    {
        if (strlen($string) === 0) {
            return;
        }

        $this->root = $this->addNode($this->root, $string, $value);
    }

    public function delete(string $string)
    {
        $this->put($string, null);
    }

    public function contains(string $string) : bool
    {
        return $this->get($string) !== null;
    }

    public function getKeys() : Vector<string>
    {
        $stack = new Vector();
        $this->addKeysStartingAtNode($this->root, $stack);
        return $stack;
    }

    /*
     * Operates in O(P + S) or Omega(1) where P is the prefix for all word and S is the length of all suffixes.
     */
    public function getKeysWithPrefix(string $prefix) : Vector<string>
    {
        $stack = new Vector();
        if (strlen($prefix) === 0 || $this->root === null) {
            return $stack;
        }

        $node = $this->getLastNodeForString($this->root, $prefix);

        if ($node === null) {
            return $stack;
        }

        if ($node->value !== null) {
            $stack[] = $prefix;
        }

        $this->addKeysStartingAtNode($node->middleChild, $stack, $prefix);
        return $stack;
    }

    public function getLongestPrefixOf(string $string) : string
    {
        $this->get($string);
        return substr($string, 0, $this->longestPrefixOffset);
    }

    /*
     * Operates in O(W) or Omega(1) time where W is the length of the word to locate.
     */
    private function getLastNodeForString(?TreeNode $node, string $string, int $offset = 0) : ?TreeNode
    {
        if ($node === null) {
            return null;
        }

        $letterComparison = $this->compareAscii($string[$offset], $node->key);

        if ($letterComparison < 0) {

            $this->setLongestPrefixOffsetIfNull($node->leftChild, $offset);
            return $this->getLastNodeForString($node->leftChild, $string, $offset);

        } elseif ($letterComparison > 0) {

            $this->setLongestPrefixOffsetIfNull($node->rightChild, $offset);
            return $this->getLastNodeForString($node->rightChild, $string, $offset);

        } elseif ($offset < strlen($string)-1) {

            $this->setLongestPrefixOffsetIfNull($node->middleChild, $offset+1);
            return $this->getLastNodeForString($node->middleChild, $string, $offset+1);

        } else {

            $this->longestPrefixOffset = $offset+1;
            return $node;

        }
    }

    private function setLongestPrefixOffsetIfNull(?TreeNode $node, int $offset)
    {
        if ($node === null) {
            $this->longestPrefixOffset = $offset;
        }
    }

    private function addNode(?TreeNode $node, string $string, ?int $value, int $offset = 0) : TreeNode
    {
        $letter = $string[$offset];
        if ($node === null) {

            $node = new TreeNode();
            $node->key = $letter;

        }

        $this->attachChildNodeByComparison($node, $string, $value, $offset);
        return $node;
    }

    /**
     * Operates in Theta(W) time where W is the length of the word being added.
     */
    private function attachChildNodeByComparison(TreeNode &$node, string $string, ?int $value, int $offset)
    {
        $letterComparison = $this->compareAscii($string[$offset], $node->key);

        if ($letterComparison < 0) {

            $leftChild = $this->addNode($node->leftChild, $string, $value, $offset);
            $node->attachLeftChild($leftChild);

        } elseif ($letterComparison > 0) {

            $rightChild = $this->addNode($node->rightChild, $string, $value, $offset);
            $node->attachRightChild($rightChild);

        } elseif ($offset < strlen($string)-1) {

            $middleChild = $this->addNode($node->middleChild, $string, $value, $offset+1);
            $node->attachMiddleChild($middleChild);

        } else {
            $node->value = $value;
        }
    }

    private function compareAscii(string $key1, string $key2)
    {
        if (ord($key1) < ord($key2)) {
            return -1;
        } elseif (ord($key1) > ord($key2)) {
            return 1;
        } else {
            return 0;
        }
    }

    /*
     * Operates in Theta(N) time where N is the size of the Trie at the given $node.
     */
    private function addKeysStartingAtNode(?TreeNode $node, Vector<string> &$stack, string $string = '')
    {
        if ($node === null) {
            return;
        }

        if ($node->value !== null) {
            $stack[] = $string . $node->key;
        }

        $this->addKeysStartingAtNode($node->leftChild, $stack, $string);
        $this->addKeysStartingAtNode($node->middleChild, $stack, $string . $node->key);
        $this->addKeysStartingAtNode($node->rightChild, $stack, $string);
    }
}