Implement Trie (Prefix Tree)

Problem

A trie (pronounced as "try") or prefix tree is a tree data structure used to efficiently store and retrieve keys in a dataset of strings. There are various applications of this data structure, such as autocomplete and spellchecker.

Implement the Trie class:

  • Trie() Initializes the trie object.
  • void insert(String word) Inserts the string word into the trie.
  • boolean search(String word) Returns true if the string word is in the trie (i.e., was inserted before), and false otherwise.
  • boolean startsWith(String prefix) Returns true if there is a previously inserted string word that has the prefix prefix, and false otherwise.

Example 1:

Input
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
Output
[null, null, true, false, true, null, true]

Explanation
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple");   // return True
trie.search("app");     // return False
trie.startsWith("app"); // return True
trie.insert("app");
trie.search("app");     // return True

Intuition

The task is to implement a Trie (pronounced "try") data structure, which is used to efficiently store and retrieve keys (words) in a dataset of strings. The Trie allows for quick searches and prefix matching. The Trie consists of nodes, where each node represents a character in a word. The edges between nodes represent characters, and each path from the root to a node forms a word.

Approach

Trie Node:

Implement a trienode class with attributes:
children: A dictionary mapping characters to child nodes.
endofword: A boolean indicating whether the current node marks the end of a word.
Trie Class:

Implement a Trie class with methods:
init: Initializes an empty Trie with a root node.
insert: Inserts a word into the Trie by iterating over its characters and creating nodes as needed.
search: Searches for a complete word in the Trie by traversing the Trie and checking if the endofword attribute is True.
startsWith: Checks if there is a path in the Trie that matches the given prefix.
Insertion:

During insertion, iterate over each character of the word.
If the character is not already a child of the current node, create a new child node.
Move to the next node and repeat the process until all characters are inserted.
Search:

During search, iterate over each character of the word.
If a character is not found as a child of the current node, return False.
If all characters are found, check if the endofword attribute is True to ensure the word is complete.
Prefix Matching:

During prefix matching, iterate over each character of the prefix.
If a character is not found as a child of the current node, return False.
If all characters are found, return True to indicate the existence of a path with the given prefix.

Complexity

  • Time complexity:

Insertion: The time complexity of insertion is O(m), where m is the length of the word being inserted.
Search: The time complexity of search is O(m), where m is the length of the word being searched.
StartsWith: The time complexity of prefix matching is O(m), where m is the length of the prefix.

  • Space complexity:

The space complexity of the Trie depends on the number of unique words and the length of the words. In the worst case, where there are no common prefixes among the words, the space complexity is O(n * m), where n is the number of words and m is the average length of the words.

Code

class trienode:
    def __init__(self):
        self.children = {}
        self.endofword = False
class Trie:
    def __init__(self):
        self.root = trienode()

    def insert(self, word: str) -> None:
        cur = self.root

        for c in word:
            if c not in cur.children:
                cur.children[c] = trienode()
            cur = cur.children[c]
        cur.endofword = True
    def search(self, word: str) -> bool:
        cur = self.root

        for c in word:
            if c not in cur.children:
                return False
            cur = cur.children[c]
        return cur.endofword

    def startsWith(self, prefix: str) -> bool:
        cur = self.root

        for c in prefix:
            if c not in cur.children:
                return False
            cur = cur.children[c]
        return True


# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.startsWith(prefix)