The 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 stringwordinto the trie.boolean search(String word)Returnstrueif the stringwordis in the trie (i.e., was inserted before), andfalseotherwise.boolean startsWith(String prefix)Returnstrueif there is a previously inserted stringwordthat has theprefixprefix, andfalseotherwise.
Examples
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
Constraints
- 1 <= word.length, prefix.length <= 2000
wordandprefixconsist only of lowercase English letters.- At most 3 * 10⁴ calls in total will be made to
insert,search, andstartsWith.
Prefix Tree Array solution
final class TrieNode {
var children: [TrieNode?]
var endOfWord: Bool
init() {
self.children = Array(repeating: nil, count: 26)
self.endOfWord = false
}
}
class Trie {
private let root: TrieNode
private let aAsciiValue: Int = Int(Character("a").asciiValue!)
init() {
self.root = TrieNode()
}
func insert(_ word: String) {
var curr = self.root
for c in word {
let i = Int(c.asciiValue!) - aAsciiValue
if curr.children[i] == nil {
curr.children[i] = TrieNode()
}
curr = curr.children[i]!
}
curr.endOfWord = true
}
func search(_ word: String) -> Bool {
var curr = self.root
for c in word {
let i = Int(c.asciiValue!) - aAsciiValue
if curr.children[i] == nil {
return false
}
curr = curr.children[i]!
}
return curr.endOfWord
}
func startsWith(_ prefix: String) -> Bool {
var curr = self.root
for c in prefix {
let i = Int(c.asciiValue!) - aAsciiValue
if curr.children[i] == nil {
return false
}
curr = curr.children[i]!
}
return true
}
}
Explanation
Before we start implementing Trie, let’s take a look at what we are going to do.
- We are going to create a node and
insertit as a child of the previous character for every character of the word and mark it as the end of the word. To do that, we need an additional classTrieNodethat will help us keep track ofchildrennodes andendOfWord. - When we want to
searchfor a word, we need to iterate through the word character by character and check if it exists as achildand if it is marked asendOfWord.- For example, with
insert = "apple"andsearch = "apple", the algorithm will returntruebecause this word has the same characters and is marked asendOfWord. - However, if we try
insert = "apple"andsearch = "app", the algorithm will returnfalseeven though the first three characters are the same. It does so because"app"is not marked asendOfWord.
- For example, with
- When we want to search for a word that
startsWitha prefix, we need to iterate through the word character by character but without checking forendOfWordbecause we just need to find the prefix.- For example, with
insert = "apple"and searching for a word thatstartsWith = "app", the algorithm will returntruebecause this word has the same first three characters as the word"apple".
- For example, with
Time/Space Complexity
- Time complexity: O(n) for each method call
- Space complexity: O(t)
- where
nis the length of the string andtis the total number ofTrieNodeobjects created in theTrie.
- where
Prefix Tree HashMap solution
final class TrieNode {
var children: [Character: TrieNode?]
var endOfWord: Bool
init() {
self.children = [:]
self.endOfWord = false
}
}
class Trie {
private let root: TrieNode
init() {
self.root = TrieNode()
}
func insert(_ word: String) {
var curr = self.root
for c in word {
if curr.children[c] == nil {
curr.children[c] = TrieNode()
}
curr = curr.children[c]!!
}
curr.endOfWord = true
}
func search(_ word: String) -> Bool {
var curr = self.root
for c in word {
if curr.children[c] == nil {
return false
}
curr = curr.children[c]!!
}
return curr.endOfWord
}
func startsWith(_ prefix: String) -> Bool {
var curr = self.root
for c in prefix {
if curr.children[c] == nil {
return false
}
curr = curr.children[c]!!
}
return true
}
}
Explanation
We can also solve this problem by using a HashMap for TrieNode instead of an array as we did above. This will result in less code and will be easier to read. The algorithm behind the solution stays the same, as do the time and space complexity.
Time/Space Complexity
- Time complexity: O(n) for each method call
- Space complexity: O(t)
- where
nis the length of the string andtis the total number ofTrieNodeobjects created in theTrie.
- where