164. Replace Words
š LeetCode Problem: 648. Replace Words
š Difficulty: Medium
š·ļø Topics: Trie, String, Array, Hash Table
Problem Statement
In English, we have a concept called root, which can be followed by some other word to form another longer word - let's call this word derivative. For example, when the root "help" is followed by the word "ful", we can form a derivative "helpful".
Given a dictionary consisting of many roots and a sentence consisting of words separated by spaces, replace all the derivatives in the sentence with the root forming it. If a derivative can be replaced by more than one root, replace it with the root that has the shortest length.
Return the sentence after the replacement.
Example 1:
Input: dictionary = ["cat","bat","rat"], sentence = "the cattle was rattled by the battery"
Output: "the cat was rat by the bat"
Example 2:
Input: dictionary = ["a","b","c"], sentence = "aadsfasf absbs bbab cadsfafs"
Output: "a a b c"
Example 3:
Input: dictionary = ["a", "aa", "aaa", "aaaa"], sentence = "a aa a aaaa aaa aaa aaa aaaaaa bbb baba ababa"
Output: "a a a a a a a a bbb baba a"
Constraints:
- 1 <= dictionary.length <= 1000
- 1 <= dictionary[i].length <= 100
- dictionary[i] consists of only lowercase letters
- 1 <= sentence.length <= 10^6
- sentence consists of only lowercase letters and spaces
- The number of words in sentence is in the range [1, 1000]
- The length of each word in sentence is in the range [1, 1000]
- Every two consecutive words in sentence will be separated by exactly one space
- sentence does not have leading or trailing spaces
š³ Visual Understanding - The Complete Picture
The Problem We're Solving:
Dictionary (roots): ["cat", "bat", "rat"]
Sentence: "the cattle was rattled by the battery"
Goal: Replace derivatives with their shortest roots
Word by word analysis:
"the" ā No root matches ā Keep "the"
"cattle" ā Starts with "cat" ā Replace with "cat" ā
"was" ā No root matches ā Keep "was"
"rattled" ā Starts with "rat" ā Replace with "rat" ā
"by" ā No root matches ā Keep "by"
"the" ā No root matches ā Keep "the"
"battery" ā Starts with "bat" ā Replace with "bat" ā
Result: "the cat was rat by the bat"
KEY INSIGHT: We need PREFIX MATCHING! š
Why Trie is Perfect:
APPROACH 1: Brute Force (Without Trie)
For each word in sentence:
For each root in dictionary:
Check if word starts with root
Keep track of shortest matching root
Replace word with shortest root (if found)
Problem:
- m words in sentence
- n roots in dictionary
- k = average root length
- l = average word length
Time: O(m * n * k) - TOO SLOW! š°
Example: 1000 words, 1000 roots, each length 10
= 1000 * 1000 * 10 = 10,000,000 operations!
APPROACH 2: Trie (Optimal)
Build Trie once with all roots: O(n * k)
For each word, search Trie once: O(m * l)
Total: O(n * k + m * l) - MUCH BETTER! š
Example: Same numbers
= (1000 * 10) + (1000 * 15) = 25,000 operations!
400x faster! šØ
The Trie Magic - Visual Structure:
Dictionary: ["cat", "bat", "rat"]
Build Trie:
root
/ | \
b c r
| | |
a a a
| | |
[t] [t] [t]
(bat) (cat) (rat)
[t] means: isEndOfWord = true (this is a complete root!)
Now search for "cattle":
Start at root
c ā exists ā
a ā exists ā
t ā exists ā, and isEndOfWord = true!
STOP! Found shortest root "cat"! šÆ
Don't need to check remaining "tle"!
This is why Trie is perfect:
1. Shares common prefixes (memory efficient)
2. Stops at first complete root (finds shortest)
3. Traversal is O(word length), not O(dictionary size)
Complete Visual Walkthrough:
Dictionary: ["cat", "bat", "rat"]
Sentence: "the cattle was rattled by the battery"
STEP 1: Build Trie (ONE TIME)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Build Trie from Dictionary ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā Insert "cat": ā
ā root ā c ā a ā t (mark END) ā
ā ā
ā Insert "bat": ā
ā root ā b ā a ā t (mark END) ā
ā ā
ā Insert "rat": ā
ā root ā r ā a ā t (mark END) ā
ā ā
ā Final Trie: ā
ā root ā
ā / | \ ā
ā b c r ā
ā | | | ā
ā a a a ā
ā | | | ā
ā [t][t][t] ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
STEP 2: Process Each Word
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Word 1: "the" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Search in Trie: ā
ā root ā t? ā NOT FOUND ā ā
ā Result: Keep "the" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Word 2: "cattle" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Search in Trie: ā
ā root ā c ā exists ā ā
ā ā a ā exists ā ā
ā ā t ā exists ā AND isEnd = true! ā
ā ā
ā Found root "cat" at position 3 ā
ā Stop here! Don't check "tle" ā
ā ā
ā Result: Replace "cattle" with "cat" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Word 3: "was" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Search in Trie: ā
ā root ā w? ā NOT FOUND ā ā
ā Result: Keep "was" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Word 4: "rattled" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Search in Trie: ā
ā root ā r ā exists ā ā
ā ā a ā exists ā ā
ā ā t ā exists ā AND isEnd = true! ā
ā ā
ā Found root "rat" at position 3 ā
ā Stop here! Don't check "tled" ā
ā ā
ā Result: Replace "rattled" with "rat" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Word 5: "by" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Search in Trie: ā
ā root ā b ā exists ā ā
ā ā y? ā NOT FOUND ā ā
ā Result: Keep "by" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Word 6: "the" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Search in Trie: ā
ā root ā t? ā NOT FOUND ā ā
ā Result: Keep "the" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Word 7: "battery" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Search in Trie: ā
ā root ā b ā exists ā ā
ā ā a ā exists ā ā
ā ā t ā exists ā AND isEnd = true! ā
ā ā
ā Found root "bat" at position 3 ā
ā Stop here! Don't check "tery" ā
ā ā
ā Result: Replace "battery" with "bat" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
FINAL RESULT:
"the cat was rat by the bat" āØ
The "Shortest Root" Property:
Dictionary: ["a", "aa", "aaa"]
Word: "aaaaa"
Search in Trie:
root
|
[a] ā isEnd = true (root "a")
|
[a] ā isEnd = true (root "aa")
|
[a] ā isEnd = true (root "aaa")
Step 1: Check 'a'
ā Found! isEnd = true
ā Return "a" immediately! ā
We DON'T continue to check "aa" or "aaa"!
Why? We want the SHORTEST root!
This is the beauty of Trie:
- First complete word found = shortest prefix
- No need to search further
- Natural shortest-first property! šÆ
Result: "aaaaa" ā "a"
š§ The AHA Moment - Why Trie Wins
Understanding the Search Operation:
KEY INSIGHT: Trie search for PREFIX is different from full word search!
FULL WORD SEARCH (Problems 161-162):
- Traverse entire word
- Check isEndOfWord at END
- Need exact match
Example: Search "cat" in dictionary
root ā c ā a ā t ā check isEnd
PREFIX SEARCH (This Problem):
- Traverse word character by character
- Check isEndOfWord at EACH STEP
- Stop at FIRST match!
Example: Search "cattle" for prefix
root ā c ā check isEnd? NO, continue
ā a ā check isEnd? NO, continue
ā t ā check isEnd? YES, STOP! ā
Found prefix "cat", don't need "tle"!
This is the CRITICAL difference! š
Visual Comparison - Prefix vs Full Word:
Dictionary: ["cat", "category"]
Trie Structure:
root
|
c
|
a
|
[t] ā Root "cat" ends here
|
e
|
g
|
o
|
r
|
[y] ā Root "category" ends here
Search "cattle":
Step 1: c ā found, isEnd? NO
Step 2: a ā found, isEnd? NO
Step 3: t ā found, isEnd? YES! ā
STOP HERE! Return "cat"
We found the shortest root automatically!
Don't need to check remaining "tle"!
Search "category":
All steps match, isEnd at 'y'
Return "category" (exact match)
Search "catalog":
c ā a ā t ā isEnd? YES!
Return "cat" (shortest root)
Don't check "alog"!
The Trie NATURALLY gives us shortest prefix! šÆ
The Algorithm Flow:
PHASE 1: Build Trie
For each root in dictionary:
Insert into Trie
Mark end of root
Time: O(n * k) where n = roots, k = avg length
Done ONCE! ā
PHASE 2: Process Sentence
For each word in sentence:
Search in Trie for shortest prefix:
Traverse character by character
If isEndOfWord found ā STOP, return this prefix
If no match ā return original word
Append result to output
Time: O(m * l) where m = words, l = avg length
Total: O(n * k + m * l) - Linear in input size! š
šÆ Solution Approaches
Approach 1: Brute Force
Idea: For each word, try all roots and find shortest match.
class Solution {
public String replaceWords(List<String> dictionary, String sentence) {
String[] words = sentence.split(" ");
StringBuilder result = new StringBuilder();
for (String word : words) {
String shortest = word; // Default to original
// Try each root
for (String root : dictionary) {
// Does word start with this root?
if (word.startsWith(root)) {
// Keep shortest root
if (root.length() < shortest.length()) {
shortest = root;
}
}
}
if (result.length() > 0) {
result.append(" ");
}
result.append(shortest);
}
return result.toString();
}
}
Analysis: - Time Complexity: O(m * n * k) where m = words, n = roots, k = avg root length - Space Complexity: O(1) excluding output - Pros: Simple, no extra data structure - Cons: Very slow for large inputs, repeated comparisons
Approach 2: HashSet Optimization
Idea: Use HashSet for O(1) root lookup, check all prefixes.
class Solution {
public String replaceWords(List<String> dictionary, String sentence) {
Set<String> rootSet = new HashSet<>(dictionary);
String[] words = sentence.split(" ");
StringBuilder result = new StringBuilder();
for (String word : words) {
String replacement = word;
// Try all possible prefixes from shortest
for (int i = 1; i <= word.length(); i++) {
String prefix = word.substring(0, i);
if (rootSet.contains(prefix)) {
replacement = prefix;
break; // Found shortest!
}
}
if (result.length() > 0) {
result.append(" ");
}
result.append(replacement);
}
return result.toString();
}
}
Analysis: - Time Complexity: O(m * l²) where l = avg word length (substring creation) - Space Complexity: O(n * k) for HashSet - Pros: Better than brute force, finds shortest automatically - Cons: Creates many temporary strings, doesn't leverage prefix structure
Approach 3: Trie (Optimal Solution) ā
Idea: Build Trie with roots, search for shortest prefix per word.
class Solution {
// Trie Node - same structure as before!
private class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean isEndOfWord = false;
}
private TrieNode root = new TrieNode();
public String replaceWords(List<String> dictionary, String sentence) {
// Step 1: Build Trie with all roots
for (String root : dictionary) {
insert(root);
}
// Step 2: Process each word in sentence
String[] words = sentence.split(" ");
StringBuilder result = new StringBuilder();
for (String word : words) {
if (result.length() > 0) {
result.append(" ");
}
// Find shortest root for this word
result.append(findShortestRoot(word));
}
return result.toString();
}
// Standard Trie insert - same as before!
private void insert(String word) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (node.children[index] == null) {
node.children[index] = new TrieNode();
}
node = node.children[index];
}
node.isEndOfWord = true;
}
// NEW: Find shortest root (prefix) for a word
private String findShortestRoot(String word) {
TrieNode node = root;
StringBuilder prefix = new StringBuilder();
for (char ch : word.toCharArray()) {
int index = ch - 'a';
// Character not in Trie? No root exists!
if (node.children[index] == null) {
return word; // Return original word
}
// Move to next character
prefix.append(ch);
node = node.children[index];
// Found a complete root? STOP HERE!
if (node.isEndOfWord) {
return prefix.toString(); // Return shortest root
}
}
// Traversed entire word, no root found
return word;
}
}
Analysis: - Time Complexity: O(n * k + m * l) where n = roots, k = avg root length, m = words, l = avg word length - Space Complexity: O(n * k) for Trie structure - Pros: Optimal time, natural prefix matching, scalable - Cons: More complex implementation, extra space
š Detailed Dry Run
Dictionary: ["cat", "bat", "rat"]
Sentence: "the cattle was rattled by the battery"
PHASE 1: Build Trie
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
BUILD TRIE - INSERT ALL ROOTS
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Insert "cat":
root
|
c
|
a
|
[t] ā isEndOfWord = true
Insert "bat":
root
/ \
b c
| |
a a
| |
[t] [t]
Insert "rat":
root
/|\\
b c r
| | |
a a a
| | |
[t][t][t]
Final Trie built! ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
PHASE 2: Process Each Word
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
WORD 1: "the"
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
findShortestRoot("the"):
Start: node = root, prefix = ""
Character 't':
index = 't' - 'a' = 19
node.children[19] exists? NO! ā
Return "the" (no root found)
Result: "the"
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
WORD 2: "cattle"
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
findShortestRoot("cattle"):
Start: node = root, prefix = ""
Character 'c':
index = 'c' - 'a' = 2
node.children[2] exists? YES ā
prefix = "c"
node = node.children[2]
node.isEndOfWord? NO
Character 'a':
index = 'a' - 'a' = 0
node.children[0] exists? YES ā
prefix = "ca"
node = node.children[0]
node.isEndOfWord? NO
Character 't':
index = 't' - 'a' = 19
node.children[19] exists? YES ā
prefix = "cat"
node = node.children[19]
node.isEndOfWord? YES! āā
STOP! Return "cat"
Result: "cat" (replaced "cattle" with root)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
WORD 3: "was"
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
findShortestRoot("was"):
Start: node = root, prefix = ""
Character 'w':
index = 'w' - 'a' = 22
node.children[22] exists? NO! ā
Return "was" (no root found)
Result: "was"
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
WORD 4: "rattled"
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
findShortestRoot("rattled"):
Start: node = root, prefix = ""
Character 'r':
index = 'r' - 'a' = 17
node.children[17] exists? YES ā
prefix = "r"
node = node.children[17]
node.isEndOfWord? NO
Character 'a':
index = 'a' - 'a' = 0
node.children[0] exists? YES ā
prefix = "ra"
node = node.children[0]
node.isEndOfWord? NO
Character 't':
index = 't' - 'a' = 19
node.children[19] exists? YES ā
prefix = "rat"
node = node.children[19]
node.isEndOfWord? YES! āā
STOP! Return "rat"
Result: "rat" (replaced "rattled" with root)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
WORD 5: "by"
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
findShortestRoot("by"):
Start: node = root, prefix = ""
Character 'b':
index = 'b' - 'a' = 1
node.children[1] exists? YES ā
prefix = "b"
node = node.children[1]
node.isEndOfWord? NO
Character 'y':
index = 'y' - 'a' = 24
node.children[24] exists? NO! ā
Return "by" (no root found)
Result: "by"
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
WORD 6: "the"
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
(Same as Word 1)
Result: "the"
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
WORD 7: "battery"
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
findShortestRoot("battery"):
Start: node = root, prefix = ""
Character 'b':
index = 'b' - 'a' = 1
node.children[1] exists? YES ā
prefix = "b"
node = node.children[1]
node.isEndOfWord? NO
Character 'a':
index = 'a' - 'a' = 0
node.children[0] exists? YES ā
prefix = "ba"
node = node.children[0]
node.isEndOfWord? NO
Character 't':
index = 't' - 'a' = 19
node.children[19] exists? YES ā
prefix = "bat"
node = node.children[19]
node.isEndOfWord? YES! āā
STOP! Return "bat"
Result: "bat" (replaced "battery" with root)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
FINAL SENTENCE:
Words: ["the", "cat", "was", "rat", "by", "the", "bat"]
Joined: "the cat was rat by the bat" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Key Observations:
1. STOP at first isEndOfWord
- "cattle" ā stopped at "cat" (didn't check "tle")
- "rattled" ā stopped at "rat" (didn't check "tled")
- "battery" ā stopped at "bat" (didn't check "tery")
2. No root found ā keep original
- "the", "was", "by" unchanged
3. Efficient search
- Each word searched in O(word length)
- Not O(dictionary size)!
4. StringBuilder for result
- Efficient string building
- Add space between words
5. One-time Trie build
- O(total root characters)
- Reused for all words! ā
š Complexity Analysis
Approach 3: Trie Solution (Optimal)
Time Complexity:
n = number of roots in dictionary
k = average length of roots
m = number of words in sentence
l = average length of words
Build Trie:
- Insert n roots
- Each root: O(k)
- Total: O(n * k)
Process Sentence:
- Split sentence: O(sentence length)
- Search m words
- Each word: O(l) in worst case
- Total: O(m * l)
Overall: O(n * k + m * l) āØ
Compare with brute force:
- For each word, check all roots
- O(m * n * min(k, l))
Trie is much better when n is large! š
Space Complexity:
Trie space:
- Worst case: all roots have unique characters
- O(n * k) for n roots of length k
- Best case: roots share prefixes
- Still O(n * k) in worst case
Result string:
- O(sentence length)
- Same as input size
Overall: O(n * k + sentence length) š¦
The Trie space is justified by:
- Faster search
- Natural prefix sharing
- Automatic shortest-first property
šÆ Pattern Recognition
The Three Trie Search Patterns:
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā PATTERN 1: EXACT WORD SEARCH ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Use Case: Dictionary lookup ā
ā Problems: 161 (Implement Trie) ā
ā ā
ā Algorithm: ā
ā 1. Traverse entire word ā
ā 2. Check isEndOfWord at END ā
ā 3. Return true/false ā
ā ā
ā Example: Search "cat" ā
ā root ā c ā a ā t (check isEnd here only) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā PATTERN 2: PREFIX SEARCH (SHORTEST) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Use Case: Root replacement, autocomplete ā
ā Problems: 164 (Replace Words) ā
ā ā
ā Algorithm: ā
ā 1. Traverse character by character ā
ā 2. Check isEndOfWord at EACH STEP ā
ā 3. STOP at first match (shortest) ā
ā ā
ā Example: Search "cattle" for prefix ā
ā root ā c (check) ā a (check) ā t (FOUND! Stop) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā PATTERN 3: WILDCARD SEARCH ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Use Case: Pattern matching with wildcards ā
ā Problems: 162 (Design Add and Search Words) ā
ā ā
ā Algorithm: ā
ā 1. Traverse with backtracking ā
ā 2. Handle '.' by trying all children ā
ā 3. Check isEndOfWord at END ā
ā ā
ā Example: Search "c.t" ā
ā root ā c ā try all 26 children ā t (check end) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
KEY INSIGHT:
Same Trie structure, different search strategies! š
Related Problems:
1. Implement Trie (208) - Basic structure
Pattern: Exact word search
2. Design Add and Search Words (211)
Pattern: Wildcard search
3. Replace Words (648) - This problem!
Pattern: Prefix search (shortest)
4. Word Search II (212) - Board + Trie
Pattern: Backtracking + Trie
5. Longest Word in Dictionary (720)
Pattern: Prefix building
6. Search Suggestions System (1268)
Pattern: Prefix search (all matches)
All use the SAME Trie structure! šÆ
š Common Mistakes
ā Mistake 1: Checking isEnd only at word end
// WRONG!
private String findShortestRoot(String word) {
TrieNode node = root;
StringBuilder prefix = new StringBuilder();
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (node.children[index] == null) return word;
prefix.append(ch);
node = node.children[index];
}
// Only checking at end - TOO LATE!
if (node.isEndOfWord) {
return prefix.toString();
}
return word;
}
// CORRECT! ā
private String findShortestRoot(String word) {
TrieNode node = root;
StringBuilder prefix = new StringBuilder();
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (node.children[index] == null) return word;
prefix.append(ch);
node = node.children[index];
// Check at EACH step!
if (node.isEndOfWord) {
return prefix.toString(); // Stop immediately!
}
}
return word;
}
ā Mistake 2: Not building result efficiently
// WRONG! - String concatenation is O(n²)
String result = "";
for (String word : words) {
result += findRoot(word) + " "; // SLOW!
}
// CORRECT! ā - StringBuilder is O(n)
StringBuilder result = new StringBuilder();
for (String word : words) {
if (result.length() > 0) {
result.append(" ");
}
result.append(findRoot(word));
}
ā Mistake 3: Forgetting space handling
// WRONG! - Extra space at end
for (String word : words) {
result.append(findRoot(word));
result.append(" "); // Adds space after last word!
}
// CORRECT! ā - Add space BETWEEN words only
for (String word : words) {
if (result.length() > 0) {
result.append(" ");
}
result.append(findRoot(word));
}
ā Mistake 4: Not handling "no root found" case
// WRONG! - What if no root found?
private String findShortestRoot(String word) {
TrieNode node = root;
StringBuilder prefix = new StringBuilder();
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (node.children[index] == null) {
// Missing return statement!
}
prefix.append(ch);
node = node.children[index];
if (node.isEndOfWord) {
return prefix.toString();
}
}
// Missing default case!
}
// CORRECT! ā - Return original word
private String findShortestRoot(String word) {
TrieNode node = root;
StringBuilder prefix = new StringBuilder();
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (node.children[index] == null) {
return word; // No root found
}
prefix.append(ch);
node = node.children[index];
if (node.isEndOfWord) {
return prefix.toString();
}
}
return word; // Default case
}
š Quick Revision Notes
šÆ Core Concept
Replace Words uses Trie for prefix matching. Build Trie with all roots (O(nk)), then for each word find shortest matching prefix (O(ml)). Key difference from standard Trie: check isEndOfWord at EACH step, not just at end. STOP at first match = shortest root automatically! Better than brute force O(mnk).
ā” Quick Implementation
import java.util.Arrays;
import java.util.List;
class TrieNode {
TrieNode[] children;
boolean isEnd;
public TrieNode() {
// Just space allocated. All are NULLs.
// For example, this.children[0] = null, this.children[1] = null and so on till this.children[25] = null
// Why null? We will initialize with new TrieNode() only when character comes. Why unncessary space before char comes?
// Check insert method. Only when character comes, we go and initialize that element in the array.
// For binary numbers, you do need 26 (as its just 0 or 1). Also, isEnd is not required.
// But lets use the same for all problems to have it generic.
this.children = new TrieNode[26];
this.isEnd = false;
}
}
public class Solution {
TrieNode root;
public String replaceWords(List<String> dictionary, String sentence) {
root = new TrieNode();
// Build Trie with dictionary list.
for(String word : dictionary) {
insert(word);
}
// Build the result now.
StringBuilder sb = new StringBuilder();
for(String word : sentence.split(" ")) {
String match = checkRootWord(word);
sb.append(match).append(" ");
}
return sb.toString().trim();
}
private String checkRootWord(String word) {
TrieNode curr = root;
StringBuilder sb = new StringBuilder();
for(char ch : word.toCharArray()) {
int index = ch - 'a';
if(curr.children[index] == null) {
return word; // char not found. Immediately return
}
curr = curr.children[index];
// if matches, append the existing character from Trie
sb.append(ch);
// If root is found at any character, return immediately.
if(curr.isEnd) {
return sb.toString();
}
}
return word;
}
private void insert(String word) {
TrieNode curr = root;
for(char ch : word.toCharArray()) {
int index = ch - 'a';
if(curr.children[index] == null) {
curr.children[index] = new TrieNode();
}
curr = curr.children[index];
}
curr.isEnd = true;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.replaceWords(Arrays.asList(new String[] {"cat","bat","rat"}), "the cattle was rattled by the battery")); // "the cat was rat by the bat"
System.out.println(s.replaceWords(Arrays.asList(new String[] {"a","b","c"}), "aadsfasf absbs bbab cadsfafs")); // "a a b c"
}
}
š Key Insights
Prefix vs Full Word Search:
Full Word (161): Check isEnd at LAST character
"cat" ā c ā a ā t (check here only)
Prefix (164): Check isEnd at EVERY character
"cattle" ā c (check) ā a (check) ā t (FOUND!)
STOP at first match = shortest automatically! šÆ
Why Trie Wins:
Without Trie:
For each word: try all roots
Time: O(m * n * k)
With Trie:
For each word: navigate Trie once
Time: O(m * l)
Trie eliminates redundant comparisons! š
The Search Pattern:
for (char ch : word.toCharArray()) {
// 1. Path exists?
if (node.children[idx] == null) return word;
// 2. Move forward
prefix.append(ch);
node = node.children[idx];
// 3. Complete root? STOP!
if (node.isEndOfWord) return prefix.toString();
}
This is the magic formula! āØ
šŖ Memory Hooks
"Check isEnd at every step, not just the end!"
"First match = shortest match!"
"No path? Return original!"
"StringBuilder for efficiency!" š„
ā ļø Don't Forget
- Check
isEndOfWordat EACH character (not just end!) - STOP at first complete root (shortest!)
- Handle "no root found" case (return original)
- Use StringBuilder for result building
- Add space BETWEEN words only (not at end)
- Insert happens ONCE (build phase)
- Search happens for EVERY word (query phase)
š Congratulations!
You've mastered Trie Prefix Matching!
What You Learned:
ā
Prefix search - different from exact word search
ā
Check at each step - not just at end
ā
Shortest-first property - automatic with Trie
ā
O(nk + ml) complexity - optimal solution
ā
StringBuilder efficiency - for string building
ā
Pattern recognition - three Trie search types
The Three Trie Search Patterns:
Problem 161 (Implement Trie):
Pattern: EXACT WORD SEARCH
Check: isEndOfWord at END only
Problem 162 (Add and Search):
Pattern: WILDCARD SEARCH
Check: Backtracking with '.'
Problem 164 (Replace Words):
Pattern: PREFIX SEARCH
Check: isEndOfWord at EACH STEP
Same structure, different strategies! šÆ
Interview Mastery:
Interviewer: "Replace derivatives with roots"
Your response:
"Classic Trie prefix matching!
Approach:
1. Build Trie with all roots
2. For each word, find shortest matching prefix
3. Check isEndOfWord at EACH step (not just end)
4. STOP at first match
Why Trie:
- Natural prefix structure
- Automatic shortest-first
- O(n*k + m*l) vs O(m*n*k) brute force
- Much faster for large dictionaries
Key insight:
- Different from exact word search
- Must check isEnd at every character
- First complete root = shortest root
Implementation:
- Standard Trie structure
- Modified search with early stop
- StringBuilder for efficiency
Complexity:
- Time: O(n*k + m*l)
- Space: O(n*k) for Trie
Much better than trying all roots per word!"
Shows complete understanding! šÆ
Your Trie Journey So Far:
Problem 161: Basic Trie ā
- Insert, Search, StartsWith
- Foundation concepts
Problem 162: Wildcard Trie ā
- Backtracking with '.'
- Recursive search
Problem 163: Binary Trie ā
- 2 children (not 26)
- XOR optimization
Problem 164: Prefix Matching Trie ā
- Check at each step
- Shortest-first property
You're becoming a Trie expert! šāØ
Keep this momentum going! You're mastering Trie patterns! š