Build a data structure that can store words and search them, but with a twist: the search function must support the dot character . which can match any single letter.
Example:
addWord("bad")
addWord("dad")
addWord("mad")
search("pad") → false
search("bad") → true
search(".ad") → true
search("b..") → true
Problem Requirements
You must implement a class with two methods:
addWord(word)
Stores a word in the data structure.
search(pattern)
Returns true if:
- the exact word exists, OR
- the pattern matches using
.wildcard characters
Constraints to keep in mind:
- Words consist of lowercase English letters
- The number of operations can be large (thousands to tens of thousands)
- Efficiency matters — you cannot search linearly through all words every time
Why a Simple Approach Fails
A naïve strategy might be:
- Store all words in a list
- During search, compare pattern to every stored word
This becomes too slow because:
- Each search becomes O(n * m)
n = number of stored words
m = word length - With thousands of operations, this times out
So we need something faster.
Logic in Simple English (Core Idea)
To solve this efficiently, we use a Trie, a special tree for storing words character by character.
Why a Trie works:
- Words share prefixes, so storage is efficient.
- Searching letter-by-letter is fast.
- When encountering
., we can explore all possible child paths. - This makes wildcard searching manageable without scanning all words.
So the idea becomes:
- Store each word in a Trie
- For search:
- If the current character is a letter, follow that child path
- If the character is
., try all children recursively - If we reach the end of the pattern and the node marks the end of a valid word, return true
- Otherwise return false
This gives fast add and flexible search.
Step-by-Step Solution Approach
For addWord(word):
- Start at root node
- For each character:
- If child doesn’t exist, create it
- Move into that child
- Mark the final node as end-of-word
For search(pattern):
- Use depth-first search (DFS)
- At each index:
- If normal letter: follow matching child
- If
., recursively try all children
- If index reaches end and node is end-of-word → return true
- If no path matches → return false
Java Implementation
class WordDictionary {
private static class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean isEndOfWord = false;
}
private final TrieNode root;
public WordDictionary() {
root = new TrieNode();
}
public void addWord(String word) {
TrieNode current = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (current.children[index] == null) {
current.children[index] = new TrieNode();
}
current = current.children[index];
}
current.isEndOfWord = true;
}
public boolean search(String word) {
return dfsSearch(word, 0, root);
}
private boolean dfsSearch(String word, int index, TrieNode node) {
if (index == word.length()) {
return node.isEndOfWord;
}
char c = word.charAt(index);
if (c == '.') {
for (TrieNode child : node.children) {
if (child != null && dfsSearch(word, index + 1, child)) {
return true;
}
}
return false;
} else {
int idx = c - 'a';
TrieNode next = node.children[idx];
return next != null && dfsSearch(word, index + 1, next);
}
}
}
Dry Run Example
Let’s walk through this sequence:
addWord("bad")
addWord("dad")
addWord("mad")
search(".ad")
Adding “bad”
- Insert b → a → d
- Mark final node as end-of-word
Adding “dad”
- Insert d → a → d
- Mark final node
Adding “mad”
- Insert m → a → d
- Mark final node
Searching “.ad”
Index 0: .
- Explore all children: b, d, m
Branch 1: b-path
Index 1: ‘a’ → matches
Index 2: ‘d’ → matches
End reached → node is end-of-word → return true
Since one path succeeds, final result = true
Time and Space Complexity
addWord
Time: O(L)
Space: O(L) worst case
L = word length
search
Worst-case Time: O(26^L) when many dots
Typical Time: O(L) for exact match, much faster than brute force
Space
Trie uses up to 26 branches per node
Common Mistakes and How to Avoid Them
❌ Storing words in a list → too slow
✅ Use Trie
❌ Returning false when encountering . too early
✅ Try all child nodes
❌ Forgetting to check end-of-word flag
✅ A full pattern match must end on a marked node
❌ Assuming all words have same length
✅ Subarrays vary; search must respect length
