Leetcode 208. Implement Trie (Prefix Tree)

Problem Explanation:

The problem is to implement a specialized kind of tree data structure called a Trie, and provide it with three functions: insert, search, and startsWith. A Trie, also known as a Prefix Tree, is a kind of search tree that is used to store a dynamic set or associative array where the keys are usually strings. Unlike a binary search tree, no node in the tree stores the key associated with that node; instead, its position in the tree defines the key with which it is associated.

Example:

For instance, let's take the Trie instance trie to which the string "apple" is inserted. Now, the Trie object should have the "apple" string stored in it. Thus, running a search function for the string "apple" should return true while searching string "app" should return false as it hasn't been inserted into the Trie. However, the startsWith function should return true when queried with "app" as the string "apple" which is present in the Trie starts with "app".

Solution Approach:

The solution uses a TrieNode struct to represent each node in the Trie. Each TrieNode contains an array of children, which are points to other TrieNodes, and a boolean value isWord, which is true when the TrieNode marks the end of a valid word.

The insert function iterates over each character in the input word and navigates the Trie accordingly, creating new TrieNodes as necessary, and sets isWord to true at the final character.

The search function is similar, but instead of creating new nodes, it returns false if it fails to find the next node. It also checks that the isWord of the final character is set to true.

The startsWith function is identical to search, but doesn't check that isWord is true.

C++ Solution:

1
2c++
3struct TrieNode {
4  vector<shared_ptr<TrieNode>> children;
5  bool isWord = false;
6  TrieNode() : children(26) {}
7};
8
9class Trie {
10 public:
11  void insert(const string& word) {
12    shared_ptr<TrieNode> node = root;
13    for (const char c : word) {
14      const int i = c - 'a';
15      if (node->children[i] == nullptr)
16        node->children[i] = make_shared<TrieNode>();
17      node = node->children[i];
18    }
19    node->isWord = true;
20  }
21
22  bool search(const string& word) {
23    shared_ptr<TrieNode> node = find(word);
24    return node != nullptr && node->isWord;
25  }
26
27  bool startsWith(const string& prefix) {
28    return find(prefix) != nullptr;
29  }
30
31 private:
32  shared_ptr<TrieNode> root = make_shared<TrieNode>();
33
34  shared_ptr<TrieNode> find(const string& prefix) {
35    shared_ptr<TrieNode> node = root;
36    for (const char c : prefix) {
37      const int i = c - 'a';
38      if (node->children[i] == nullptr)
39        return nullptr;
40      node = node->children[i];
41    }
42    return node;
43  }
44};

Python Solution:

1
2python
3class TrieNode(object):
4    def __init__(self):
5        self.isWord = False
6        self.children = collections.defaultdict(TrieNode)
7
8class Trie(object):
9
10    def __init__(self):
11        self.root = TrieNode()
12
13    def insert(self, word):
14        node = self.root
15        for w in word:
16            node = node.children[w]
17        node.isWord = True
18
19    def search(self, word):
20        node = self.root
21        for w in word:
22            if w not in node.children:
23                return False
24            node = node.children[w]
25        return node.isWord
26
27    def startsWith(self, prefix):
28        node = self.root
29        for w in prefix:
30            if w not in node.children:
31                return False
32            node = node.children[w]
33        return True

Java Solution:

1
2java
3class Trie {
4    private TrieNode root;
5
6    public Trie() {
7        root = new TrieNode();
8    }
9
10    public void insert(String word) {
11        TrieNode node = root;
12        for (int i = 0; i < word.length(); i++) {
13            char currentChar = word.charAt(i);
14            if (!node.containsKey(currentChar)) {
15                node.put(currentChar, new TrieNode());
16            }
17            node = node.get(currentChar);
18        }
19        node.setEnd();
20    }
21
22    private TrieNode searchPrefix(String word) {
23        TrieNode node = root;
24        for (int i = 0; i < word.length(); i++) {
25           char curLetter = word.charAt(i);
26           if (node.containsKey(curLetter)) {
27               node = node.get(curLetter);
28           } else {
29               return null;
30           }
31        }
32        return node;
33    }
34
35    public boolean search(String word) {
36       TrieNode node = searchPrefix(word);
37       return node != null && node.isEnd();
38    }
39    
40    public boolean startsWith(String prefix) {
41        TrieNode node = searchPrefix(prefix);
42        return node != null;
43    }
44}

JavaScript Solution:

1
2javascript
3var TrieNode = function(){
4    this.END = false;
5    this.children = {};
6};
7var Trie = function() {
8    this.root = new TrieNode();
9};
10
11Trie.prototype.insert = function(word) {
12    let node = this.root;
13    for(let i = 0;i < word.length;i++){
14        if(node.children[word[i]] == undefined){
15            node.children[word[i]] = new TrieNode();
16        }
17        node = node.children[word[i]];
18    }
19    node.END = true;
20};
21
22Trie.prototype.search = function(word) {
23    let node = this.root;
24    for(let i = 0;i < word.length;i++){
25        if(node.children[word[i]] == undefined){
26            return false;
27        }
28        node = node.children[word[i]];
29    }
30    return node.END;
31};
32
33Trie.prototype.startsWith = function(prefix) {
34    let node = this.root;
35    for(let i = 0;i < prefix.length;i++){
36        if(node.children[prefix[i]] == undefined){
37            return false;
38        }
39        node = node.children[prefix[i]];
40    }
41    return true;
42};

C# Solution:

1
2csharp
3public class TrieNode {
4    public Dictionary<char, TrieNode> children = new Dictionary<char, TrieNode>();
5    public bool word = false;
6
7    public void Insert(char[] arr, int index) {
8        if (index == arr.Length) {
9            word = true;
10            return;
11        }
12        var c = arr[index];
13        if (!children.ContainsKey(c)) children[c] = new TrieNode();
14        children[c].Insert(arr, index + 1);
15    }
16
17    public bool Search(char[] arr, int index) {
18        if (index == arr.Length) return word;
19        var c = arr[index];
20        if (!children.ContainsKey(c)) return false;
21        return children[c].Search(arr, index + 1);
22    }
23
24    public bool StartsWith(char[] arr, int index) {
25        if (index == arr.Length) return true;
26        var c = arr[index];
27        if (!children.ContainsKey(c)) return false;
28        return children[c].StartsWith(arr, index + 1);
29    }
30}
31
32public class Trie {
33    TrieNode root;
34
35    public Trie() {
36        root = new TrieNode();
37    }
38
39    public void Insert(string word) {
40        root.Insert(word.ToCharArray(), 0);
41    }
42
43    public bool Search(string word) {
44        return root.Search(word.ToCharArray(), 0);
45    }
46
47    public bool StartsWith(string prefix) {
48        return root.StartsWith(prefix.ToCharArray(), 0);
49    }
50}

That's all for the implementation of Trie data structure and its three most common operations: insert, search, and startsWith in multiple programming languages. It's important to note that the complexity of these operations in a Trie does not depend on the number of keys, but rather on the length of the key. This makes Trie data structure particularly efficient in scenarios where we are dealing with massive sets of strings. For instance, Tries are used widely in real-world applications such as Autocomplete feature in IDEs or web browsers, spell checkers, IP routing (Longest Prefix Matching) and more.

However, one disadvantage of using Trie compared to hash table is that Trie can be more space consuming when storing fewer keys with large associated data. Also, in terms of numeric keys, tries are comparable to binary search trees whereas hash tables which are usually more useful.

Like any other data structure, Tries have their use-cases where they outshine other data structures, and it's crucial for us as software engineers to be aware of where to employ them best. Overall, having a good grasp of Trie can be a good addition to your data structure and algorithm skill set.


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫