Design Add and Search Words Data Structure
Design a dictionary that stores words and searches patterns where . can stand for any single letter.
Intuition
Keeping every word in a list makes insertion easy, but every search has to scan unrelated words and compare their characters again. A trie removes that repeated prefix work because words with the same beginning share the same path. Ordinary letters still choose one child, while . is the only moment when the search must branch. A depth-first search over those wildcard branches is correct because it explores every stored character that could occupy that exact pattern position, and the terminal flag prevents a prefix from being mistaken for a complete word.
Repeated word insertion plus prefix-shaped lookup is a strong trie signal. When one pattern symbol can match any single character, keep normal trie traversal for fixed letters and use DFS only at wildcard positions.
Approach
Store common prefixes only once
For addWord, walk from the root through one child per character, creating a node only when that edge does not exist. Mark the final node as terminal. The flag matters because the path for bad also exists inside baddie, but only an explicitly completed word should match.
Follow one edge for an ordinary letter
During search, a normal character has exactly one possible continuation. If that child is absent, no stored word can match the pattern, so the branch fails immediately. If it exists, advance both the trie node and the pattern index together.
Branch only when the pattern contains a dot
For ., recursively try every child of the current node at the next pattern position. Return as soon as one branch reaches a valid word; if every branch fails, the wildcard cannot be satisfied. When all pattern characters are consumed, return the node's terminal flag rather than accepting the path automatically.
Solution
Common pitfalls
Treating the wildcard as a literal key
node = node.children[ch]
for child in node.children.values():
A dot represents every available character at that depth. Looking up a child named . misses every valid wildcard match.
Accepting any consumed path as a word
return True
return node.is_word
Consuming the pattern may stop at a non-terminal prefix. The dictionary should match only words that were actually added.
Restarting after choosing a wildcard child
if dfs(self.root, index + 1):
if dfs(child, index + 1):
Once a wildcard chooses a character, the rest of the pattern must continue below that child. Restarting at the root combines pieces from unrelated words.
Edge cases
...The DFS explores trie paths of exactly three characters and accepts only a terminal node, so it matches a stored three-letter word but not a longer word sharing that prefix.
app after adding appleTraversal reaches the node for the prefix, but its terminal flag is false, so the search correctly returns false.
The insertion follows the existing path and sets the same terminal flag again, leaving search behaviour unchanged.