We need to build a dictionary that supports two operations: adding words and searching for them. Search queries can contain wildcard dots, where each dot matches any single letter. Searching for ".ad" should match "bad", "dad", "mad", or any three-letter word ending in "ad".
Without the wildcard, a hash set would be enough. With dots, we can't hash the query and look it up, because a pattern like "b.d" stands for 26 different strings. We have to match character by character, and at every dot position consider all possible characters.
This makes it a prefix-matching problem with branching: when we hit a dot, we explore multiple paths. A Trie (prefix tree) fits because it stores words character by character and lets a search branch at wildcard positions.
word.length <= 25 → Words are short. Each comparison or Trie walk touches at most 25 characters.At most 3 dots in search queries → This caps the branching during a wildcard search: at most 3 positions fan out into up to 26 children each, so the search stays cheap.At most 10^4 calls → Enough operations that scanning every stored word on every search becomes expensive.Lowercase English letters only → 26-character alphabet, which bounds the Trie's branching factor.Store every added word in a list, and for each search query, check every stored word against the pattern. A stored word matches if it has the same length and every character either equals the pattern character or the pattern character is a dot.
This is correct and easy to implement, but every search scans the entire list, including words whose length can't match.
addWord(word): append the word to the list.search(word): iterate through every stored word. For each stored word, check if it has the same length as the search pattern. If so, compare character by character: if the search character is a dot, it matches anything; otherwise, the characters must be equal. If all characters match, return true.The next approach organizes words by their characters in a tree, so a search only walks paths that can still match instead of scanning the whole list.
A Trie (prefix tree) stores words character by character in a tree. Each node has up to 26 children (one per lowercase letter), and a node is marked as "end of word" where a complete word terminates. Adding a word means walking down the tree, creating nodes as needed. Searching for an exact word means following its characters down the tree.
The dot wildcard maps onto this structure directly. For a regular character, we follow the single matching child (if it exists). For a dot, we try every existing child, and if any of those paths leads to a complete match, we return true. This is a DFS with backtracking: each dot branches into multiple paths, and we explore them recursively.
The branching is bounded in two ways. First, a query has at most 3 dots, so even the theoretical worst case is 26^3 = 17,576 paths. Second, we only descend into children that exist. If 3 stored words start with 'b' and 1,000 start with other letters, searching for "b.." follows 'b' once and then explores only the 3 surviving subtrees; the other 1,000 words are never touched. This pruning by shared prefix is what the list-based approach cannot do.
isEnd flag.addWord(word): start at the root. For each character in the word, compute the index (char - 'a'). If the child at that index doesn't exist, create a new node. Move to the child. After the last character, mark the current node as isEnd = true.search(word): use a recursive helper dfs(node, word, index). At each step:index == word.length, return node.isEnd (we've matched all characters, check if it's a complete word).index + 1. If any returns true, return true.