Problem Statement in English

Your task is to implement a trie data structure.


Approach

A Trie allows us to store strings in a way that allows us to search for them efficiently. Prefix searches are particularly efficient. Why? Let’s see.

First let’s build a Trie. We’ll work through an example, and hopefully then, it should make sense to you.

Let’s build a Trie for the word “trie”.

    graph TD
    A(' ') --> B('t', False)
    B --> C('r', False)

    C --> D('i', False)
    D --> E('e', True)

Let’s also add the word “trial” to the same Trie.

    graph TD
    A(' ') --> B('t', False)
    B --> C('r', False)

    C --> D('i', False)
    D --> E('e', True)

    D --> F('a', False)
    F --> G('l', True)

How about “treat”?

    graph TD
    A(' ') --> B('t', False)
    B --> C('r', False)

    C --> D('i', False)
    D --> E('e', True)

    D --> F('a', False)
    F --> G('l', True)

    C --> H('e', False)
    H --> I('a', False)
    I --> J('t', True)

And lastly, let’s add the word “great” to the same Trie.

    graph TD
    A(' ') --> B('t', False)
    B --> C('r', False)

    C --> D('i', False)
    D --> E('e', True)

    D --> F('a', False)
    F --> G('l', True)

    C --> H('e', False)
    H --> I('a', False)
    I --> J('t', True)

    A --> K('g', False)
    K --> L('r', False)
    L --> M('e', False)
    M --> N('a', False)
    N --> O('t', True)

Notice how the boolean value indicates that that particular node is the end of a word. This is quite useful when we are searching for a word, but it also serves another purpose.

For example, if we added the word “team” to the Trie, we technically have “tea” as well in the Trie, but we didn’t add it. So if “tea” is searched for, we need to return False, and these boolean values help us do exactly that.


Solution in Python


class TrieNode:
    def __init__(self) -> None:
        # A dictionary to store the child tries of the node
        self.children: dict[str, TrieNode] = {}
        # A boolean to indicate if the node is the end of a word
        self.isTerminal = False


class PrefixTree:

    def __init__(self):
        # The root of the trie
        # empty, essentially
        self.root = TrieNode()

    def insert(self, word: str) -> None:
        # Start at the root
        ptr = self.root

        # For each character in the word
        # we wish to search for
        for char in word:
            # If the character is not in the
            # children of the current Trie node
            if char not in ptr.children:
                # Add the character to the children
                # as a new Trie node
                ptr.children[char] = TrieNode()

            # Move the pointer to the child node
            # corresponding to the current character
            ptr = ptr.children[char]

        # Mark the last node as the end of a word
        ptr.isTerminal = True


    def search(self, word: str) -> bool:
        # Start at the root
        ptr = self.root

        # For each character in the word
        for char in word:
            # If the character is in the children
            # of the current Trie node
            if char in ptr.children:
                # Move the pointer to the child trie node
                ptr = ptr.children[char]
            else:
                # If the character is not in the children
                # of the current Trie node, the word is not
                # in the Trie
                return False

        # If the last node is marked as the end of a word
        # then the word is in the Trie
        return ptr.isTerminal

    def startsWith(self, prefix: str) -> bool:
        # Start at the root
        ptr = self.root

        # For each character in the word
        for char in prefix:
            # If the character is in the children
            # of the current Trie node
            if char in ptr.children:
                # Move the pointer to the child trie node
                ptr = ptr.children[char]
            # If the character is not in the children
            # of the current Trie node, the prefix is not
            else:
                # return `False`
                return False

        # if we've got this far, the prefix is in the Trie
        # return `True`
        return True

Complexity

  • Time:

    • Insert: $O(n)$
      Since we insert $n$ characters at most in the Trie.

    • Search: $O(n)$
      Since we search through $n$ characters at most in the Trie.

    • Starts With: $O(n)$
      Since we search through $n$ characters at most in the Trie.

    Where $n$ is the length of the word

  • Space: $O(n)$
    Since we store $n$ characters at most in the Trie.


And we are done.