Design Add and Search Words Data Structure
Problem
Design a data structure that supports adding new words and searching for a word, where the search word may contain the dot character '.' to represent any one letter.
- 1 ≤ word.length ≤ 500
- word in addWord consists of lowercase English letters
- word in search consists of '.' or lowercase English letters
- At most 3 * 10⁴ calls will be made to addWord and search
Example
addWord("bad"), addWord("dad"), addWord("mad"), search("pad"), search("bad"), search(".ad"), search("b..")[null, null, null, false, true, true, true]Words "bad", "dad", and "mad" are added to the data structure. Searching for "pad" returns false because it was not added. Searching for "bad" returns true as it was added. Searching for ".ad" returns true because '.' matches any letter and "bad", "dad", and "mad" all match. Searching for "b.." returns true because it matches "bad".
Approach
Straightforward Solution
A brute-force approach would store all words in a list and check each word against the search pattern, which is inefficient (O(n*m) where n is number of words and m is word length) and does not scale well.
Core Observation
A Trie is a tree-like data structure that stores words by sharing common prefixes, enabling efficient prefix-based queries. The wildcard '.' introduces branching in the search, requiring exploration of all possible child nodes at that position.
Path to Optimal
PreviewThe key recognition signals are 'add words' and 'search with wildcard .', which indicate a Trie data structure with backtracking search…
Full step-by-step walkthrough on Pro →
Optimal Approach
PreviewImplement a TrieNode class with a dictionary of children and an end_of_word flag. For addWord, insert characters sequentially, creating nodes as needed…
Full step-by-step walkthrough on Pro →
Want the full reasoning chain?
Unlock the complete walkthrough, line-by-line analysis, and recall drill.
Unlock ProTime
O(m * 26^k)
In the worst case, the search explores all possible branches for each '.' wildcard, where m is the length of the word and k is the number of wildcards. Each wildcard can branch into up to 26 children, leading to exponential branching in k. However, typical usage and pruning make this efficient in practice.
Space
O(n * m)
The Trie stores all added words, with each character represented as a node. In the worst case, all words are distinct with no shared prefixes, leading to O(n*m) nodes, where n is the number of words and m is the average word length.
Pattern Spotlight
Trie (Prefix Tree) with Backtracking for Wildcard Search
When searching with wildcards in a prefix tree, use DFS to explore all possible child nodes at wildcard positions, enabling flexible pattern matching while leveraging prefix sharing for efficiency.
Solution
| 1 | class TrieNode:
|
| 2 | def __init__(self):
|
| 3 | self.children = {}
|
| 4 | self.end_of_word = False
|
| 5 |
|
| 6 | class WordDictionary:
|
| 7 | def __init__(self):
|
| 8 | self.root = TrieNode()
|
| 9 |
|
| 10 | def addWord(self, word: str) -> None:
|
| 11 | curr = self.root
|
| 12 | for c in word:
|
| 13 | if c not in curr.children:
|
| 14 | curr.children[c] = TrieNode()
|
| 15 | curr = curr.children[c]
|
| 16 | curr.end_of_word = True
|
| 17 |
|
| 18 | def search(self, word: str) -> bool:
|
| 19 | def dfs(j, root):
|
| 20 | curr = root
|
| 21 | for i in range(j, len(word)):
|
| 22 | c = word[i]
|
| 23 | if c == ".":
|
| 24 | for child in curr.children.values():
|
| 25 | if dfs(i + 1, child):
|
| 26 | return True
|
| 27 | return False
|
| 28 | else:
|
| 29 | if c not in curr.children:
|
| 30 | return False
|
| 31 | curr = curr.children[c]
|
| 32 | return curr.end_of_word
|
| 33 |
|
| 34 | return dfs(0, self.root) |
Step-by-Step Solution
Build Trie Nodes and Insert Words to Share Prefixes
| 3 | self.children = {}
|
| 4 | self.end_of_word = False
|
| 8 | self.root = TrieNode()
|
| 11 | curr = self.root
|
| 12 | for c in word:
|
| 13 | if c not in curr.children:
|
| 14 | curr.children[c] = TrieNode()
|
| 15 | curr = curr.children[c]
|
| 16 | curr.end_of_word = True
|
Objective
To construct a prefix tree that stores all added words efficiently by sharing common prefixes.
Key Insight
A TrieNode holds a dictionary of children nodes keyed by characters and a flag indicating if a word ends at that node. Adding a word involves traversing or creating nodes for each character, enabling fast prefix-based storage and lookup. This structure reduces redundant storage and accelerates searches compared to a flat list.
Interview Quick-Check
Core Logic
Each character in the word corresponds to a child node in the Trie, creating a path that represents the word.
State & Boundaries
Marking the end_of_word flag at the last character node distinguishes complete words from prefixes.
Common Pitfalls & Bugs
Forgetting to create a new TrieNode when a character is not present leads to incorrect or incomplete Trie construction.
Explore Trie Recursively to Match Words with Wildcards
To perform a depth-first search on the Trie that supports '.' wildcard matching by exploring all possible child nodes at wildcard positions.
1 more step with full analysis available on Pro.
Line Analysis
This solution has 5 Critical lines interviewers watch for.
curr.end_of_word = True
Mark the current node as the end of a word after insertion.
This line is critical because it distinguishes complete words from mere prefixes, enabling accurate search results that differentiate between partial and full matches.
if c == ".":
Check if the current character is a wildcard '.'
This conditional is the core of wildcard handling; it forces exploration of all children nodes, which is essential to correctly match any letter at this position.
if dfs(i + 1, child):
Recursively search the remainder of the word from the next index and child node.
This recursive exploration is the heart of the backtracking search, enabling the algorithm to find any valid word matching the pattern by trying all possible continuations.
Full line-by-line criticality + rationale for all 24 lines available on Pro.
Test Your Understanding
Why does the search function use DFS and explore all children when encountering a '.' character?
See the answer with Pro.
Related Problems
Trie pattern
Don't just read it. Drill it.
Reconstruct Design Add and Search Words Data Structure from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.
Unlock the Design Add and Search Words Data Structure drill