Design Add and Search Words Data Structure
Store every word in a trie: one node per letter, with words that share a start sharing the top of the path. Each node has up to 26 children, and isEnd is set only on the node where a word ends. search walks the pattern down the trie. A letter follows one child. A . tries every child in turn and backs up after each failure. It returns true only if it uses up the pattern on a node with isEnd set.
A search returns true only if it uses up the whole pattern and stops on a node with isEnd set. Each letter of the pattern follows a child that really exists, and each . is replaced by a real child.
Problem
Build a class WordDictionary that stores words and answers pattern queries.
WordDictionary()creates an empty dictionary.addWord(word)stores a word. You can add words at any time.search(pattern)returnstrueif some stored word matches the pattern, andfalseotherwise.
A pattern matches a word only when both have the same length and every letter agrees. The one exception is ., which stands for any single letter. So b.. matches bad, and ..d matches bad, dad and mad. A prefix of a stored word does not count: ba does not match, even though it is the start of bad.
A sequence of calls: addWord(word) with lowercase letters, and search(pattern) with lowercase letters and ..
For each search call, true or false. addWord returns nothing.
pad does not start any stored word. bad is stored. .ad matches bad, dad or mad. b.. matches bad.
ba is only the start of bad, and no word ends there, so it does not match. ... has three letters, so it matches bad.
- • 1 ≤ word length ≤ 25
- • Words given to `addWord` use only lowercase letters `a` to `z`.
- • Patterns given to `search` use lowercase letters and `.`.
- • A pattern has at most 2 dots.
- • At most 10^4 calls in total to `addWord` and `search`.
First, think yourself
Before opening any solution, try to answer these on paper. Use hints only when stuck — each hint is counted.
- 1.
If the pattern has no dots, how would I check whether one stored word matches it, letter by letter?
- 2.
A
.can be any letter. If the pattern is.ad, how many different words could it be, and where do they split? - 3.
If I store
badand the pattern isba, isbaa match? What decides whether a walk through the letters counts as a word? - 4.
badandbatboth start withba. If I store them separately, do I comparebandatwice? - 5.
How many stored words would a plain list force me to check for one search if there are 10,000 of them?
Brute force approach
Optimized approach
Why does this algorithm work?
Dry run
All approaches compared
| Approach | Idea | Time | Space |
|---|---|---|---|
| Brute Force | Keep words in a list per length; compare the pattern letter by letter with each | O(N · L) | O(T) |
| Optimal | Words share a tree of letters; a plain letter follows one child, a dot tries every child | O(26^d · L) | O(T) |
Why does this belong to this pattern?
- “stores words to search later”Many words and many lookups → build one structure over the words, not a list to scan.
- “`.` matches any one letter”A wildcard lets one position take several paths → a DFS that backs up at each dot.
- “matches any previously added string”The whole word must match, so a walk that stops early on a prefix is not a match → keep an end flag on each node.
A trie alone is not enough. Implement Trie's search follows a single path, because every letter is fixed. Here a . makes the walk branch, so the search must try each child and back up after a failure, the same way a grid DFS does. The end check matters just as much: the search must stop on a word end, not just on any node it reaches. Word Search II uses this trie-plus-DFS walk too, but over letters on a board.
Don't make this mistake
This is the startsWith check from Implement Trie, not search. Dry run: bad is stored. search("ba") walks root → b → a and uses up the pattern, so the wrong line returns true. But ba was never added. Only the node where the walk stops, checked with isEnd, says whether a word ends there.
With bad added this way, the nodes for b and ba also become word ends. Then search("b") returns true, even though only bad was added.
A dot must use up exactly one letter by moving to a child. In the dry run, .at with bad and cat stored: if the dot does not move down, the walk looks for a at the root, finds no child, and returns false. But cat matches .at.
In the dry run, the first child of the root is b. For .at, b → a has no t, so the wrong version returns false and never tries c, where cat is. A dot must try every child and back up after each failure.
Want to implement this?
Write it yourself on LeetCode, then come back and compare your code with the solutions above.
Open coding workspaceHow this concept connects to System Design & C++ Systems
Top engineering interviews test whether you understand how algorithms like Trie translate into production architectures and low-level memory mechanics.