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 stringword
into the trie.boolean search(String word)
Returnstrue
if the stringword
is in the trie (i.e., was inserted before), andfalse
otherwise.boolean startsWith(String prefix)
Returnstrue
if there is a previously inserted stringword
that has theprefix
prefix, andfalse
otherwise.
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
word
andprefix
consist 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
insert
it 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 classTrieNode
that will help us keep track ofchildren
nodes andendOfWord
. - When we want to
search
for a word, we need to iterate through the word character by character and check if it exists as achild
and if it is marked asendOfWord
.- For example, with
insert = "apple"
andsearch = "apple"
, the algorithm will returntrue
because this word has the same characters and is marked asendOfWord
. - However, if we try
insert = "apple"
andsearch = "app"
, the algorithm will returnfalse
even 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
startsWith
a prefix, we need to iterate through the word character by character but without checking forendOfWord
because we just need to find the prefix.- For example, with
insert = "apple"
and searching for a word thatstartsWith = "app"
, the algorithm will returntrue
because 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
n
is the length of the string andt
is the total number ofTrieNode
objects 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
n
is the length of the string andt
is the total number ofTrieNode
objects created in theTrie
.
- where