Why Trie Exists and What Hash Map Cannot Do for Strings in DSA Go - Performance Analysis
We want to understand why tries are used for strings instead of hash maps.
What makes tries faster or better for some string tasks?
Analyze the time complexity of inserting and searching strings in a trie.
// Insert a word into trie
func (t *TrieNode) Insert(word string) {
node := t
for _, ch := range word {
if node.children[ch] == nil {
node.children[ch] = &TrieNode{children: make(map[rune]*TrieNode)}
}
node = node.children[ch]
}
node.isEnd = true
}
// Search a word in trie
func (t *TrieNode) Search(word string) bool {
node := t
for _, ch := range word {
if node.children[ch] == nil {
return false
}
node = node.children[ch]
}
return node.isEnd
}
This code inserts and searches words character by character in a trie structure.
Look at the loops that go through each character of the word.
- Primary operation: Looping through each character in the input string.
- How many times: Once per character, so number of characters in the word (length of word).
As the word gets longer, the time to insert or search grows linearly with the number of characters.
| Input Size (n = word length) | Approx. Operations |
|---|---|
| 10 | 10 character checks |
| 100 | 100 character checks |
| 1000 | 1000 character checks |
Pattern observation: The work grows directly with the length of the word, not with the number of words stored.
Time Complexity: O(m) where m is the length of the word
This means inserting or searching a word takes time proportional to its length, independent of how many words are stored.
[X] Wrong: "Hash maps always give O(1) time for string search, so tries are slower and unnecessary."
[OK] Correct: Hash maps need to hash the entire string each time, which takes O(m) time, and cannot efficiently support prefix searches or ordered traversal like tries do.
Understanding why tries exist helps you explain when to choose them over hash maps for string problems, showing deeper insight into data structures.
What if we changed the trie to use arrays instead of maps for children? How would the time complexity change?
Practice
Solution
Step 1: Understand Trie prefix search
Trie stores strings in a tree form where each node represents a character, allowing quick prefix searches.Step 2: Compare with Hash Map capabilities
Hash Map stores full strings as keys but cannot efficiently find all strings sharing a prefix without checking all keys.Final Answer:
Because Trie can efficiently find all strings with a common prefix -> Option AQuick Check:
Trie = Efficient prefix search [OK]
- Thinking Hash Map sorts keys automatically
- Believing Trie uses less memory always
- Confusing Trie nodes with full string keys
Solution
Step 1: Identify correct children type for TrieNode
Children should map characters (rune) to child nodes (pointer to TrieNode).Step 2: Check isEnd field type
isEnd should be a boolean indicating if node marks end of a word.Final Answer:
type TrieNode struct { children map[rune]*TrieNode; isEnd bool } -> Option CQuick Check:
Children map[rune]*TrieNode and isEnd bool [OK]
- Using wrong map key or value types
- Setting isEnd as string or int instead of bool
- Using slice instead of map for children
type TrieNode struct {
children map[rune]*TrieNode
isEnd bool
}
func main() {
root := &TrieNode{children: make(map[rune]*TrieNode)}
word := "cat"
node := root
for _, ch := range word {
if node.children[ch] == nil {
node.children[ch] = &TrieNode{children: make(map[rune]*TrieNode)}
}
node = node.children[ch]
}
node.isEnd = true
// Check if 'car' is in Trie
node = root
checkWord := "car"
found := true
for _, ch := range checkWord {
if node.children[ch] == nil {
found = false
break
}
node = node.children[ch]
}
if found && node.isEnd {
println("Found")
} else {
println("Not Found")
}
}Solution
Step 1: Insert 'cat' into Trie
The code adds nodes for 'c', 'a', 't' and marks 't' node as end of word.Step 2: Search for 'car' in Trie
Traversal finds 'c' and 'a' nodes but no child for 'r', so found becomes false.Final Answer:
Not Found -> Option BQuick Check:
Only 'cat' inserted, 'car' missing last node [OK]
- Assuming partial prefix means found
- Confusing isEnd flag usage
- Expecting runtime errors from nil map access
type TrieNode struct {
children map[rune]*TrieNode
isEnd bool
}
func insert(root *TrieNode, word string) {
node := root
for _, ch := range word {
if node.children[ch] == nil {
node.children[ch] = &TrieNode{}
}
node = node.children[ch]
}
node.isEnd = true
}
func main() {
root := &TrieNode{}
insert(root, "dog")
}Solution
Step 1: Check initialization of children map
Root node's children map is nil because it was not created with make(), causing panic on access.Step 2: Understand panic cause
Accessing or assigning to nil map causes runtime panic in Go.Final Answer:
children map is not initialized before use -> Option AQuick Check:
Nil map access causes panic [OK]
- Ignoring map initialization
- Thinking isEnd type causes panic
- Assuming nil pointer for root
Solution
Step 1: Understand Trie prefix traversal
In Trie, each character is a node; to find prefix "pre", traverse nodes 'p' -> 'r' -> 'e'.Step 2: Collect all words under prefix node
From the 'e' node, recursively collect all words by exploring child nodes marking word ends.Final Answer:
Trie stores words by prefix nodes, allowing direct traversal to prefix end; then collect all descendant words -> Option DQuick Check:
Trie = Direct prefix traversal + collect descendants [OK]
- Assuming Hash Map sorts keys automatically
- Thinking Trie uses hashing internally
- Confusing linked list with Hash Map storage
Ready to prove it?
Pass the test and this pattern moves unknown
