title: Design Add and Search Words Data Structure slug: design-add-and-search-words-data-structure difficulty: medium leetcode_id: 211 leetcode_url: https://leetcode.com/problems/design-add-and-search-words-data-structure/ categories: - strings - trees patterns: - trie - dfs description: | Design a data structure that supports adding new words and finding if a string matches any previously added string. Implement the `WordDictionary` class: - `WordDictionary()` Initializes the object. - `void addWord(word)` Adds `word` to the data structure, it can be matched later. - `bool search(word)` Returns `true` if there is any string in the data structure that matches `word` or `false` otherwise. `word` may contain dots `'.'` where dots can be matched with **any letter**. constraints: | - `1 <= word.length <= 25` - `word` in `addWord` consists of lowercase English letters. - `word` in `search` consist of `'.'` or lowercase English letters. - There will be at most `2` dots in `word` for `search` queries. - At most `10^4` calls will be made to `addWord` and `search`. examples: - input: | ["WordDictionary","addWord","addWord","addWord","search","search","search","search"] [[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]] output: "[null,null,null,null,false,true,true,true]" explanation: | WordDictionary wordDictionary = new WordDictionary(); wordDictionary.addWord("bad"); wordDictionary.addWord("dad"); wordDictionary.addWord("mad"); wordDictionary.search("pad"); // return False, no word matches "pad" wordDictionary.search("bad"); // return True, exact match wordDictionary.search(".ad"); // return True, matches "bad", "dad", "mad" wordDictionary.search("b.."); // return True, matches "bad" explanation: intuition: | This problem combines two classic concepts: the **Trie** (prefix tree) data structure and **depth-first search** with wildcards. Think of a Trie as an autocomplete system: each node represents a character, and following a path from root to a marked "end" node spells out a complete word. For "bad", "dad", and "mad", the Trie would have three branches from the root (for 'b', 'd', 'm'), each leading through 'a' to 'd', with the final 'd' nodes marked as word endings. The twist here is the wildcard `.` character, which can match **any** letter. When we encounter a `.` during search, we can't just follow one path — we must explore **all possible branches** at that level. This is where DFS comes in: we recursively try every child node when we see a dot. Imagine searching for ".ad" in our Trie. At the root, the `.` means we try the 'b', 'd', and 'm' branches simultaneously. Each path then continues matching 'a' and 'd' literally. If any path reaches the end and finds a complete word, we return `true`. approach: | We implement a Trie with wildcard search using DFS: **Step 1: Define the TrieNode structure** - Each node has a dictionary `children` mapping characters to child nodes - Each node has a boolean `is_end` marking whether a complete word ends here   **Step 2: Implement `addWord`** - Start at the root node - For each character in the word, create a child node if it doesn't exist - Move to that child node - After processing all characters, mark the final node as `is_end = True`   **Step 3: Implement `search` with DFS** - Use a helper function `dfs(index, node)` that searches from position `index` in the word starting at `node` - **Base case:** If `index == len(word)`, return `node.is_end` (did we reach a valid word ending?) - **Literal character:** If the current character is a letter, check if it exists in `children`. If yes, recurse; if no, return `False` - **Wildcard `.`:** Try **all** children nodes recursively. If any path returns `True`, the search succeeds   **Step 4: Return the result** - Start the DFS from index `0` and the root node - Return `True` if any valid path is found common_pitfalls: - title: Using a List or Set Instead of a Trie description: | A naive approach stores all words in a list or set and checks each word against the pattern during search. For exact matches, this works. But with wildcards, you'd need to iterate through all stored words and check character-by-character, resulting in **O(n × m)** per search where `n` is the number of words and `m` is the word length. With a Trie, exact searches are **O(m)**, and wildcard searches only branch when necessary, making it significantly faster for large dictionaries. wrong_approach: "Store words in a list, iterate and match on search" correct_approach: "Use a Trie for O(m) prefix-based lookup" - title: Not Handling the Wildcard Correctly description: | When encountering `.`, you must explore **all** children, not just one. A common mistake is treating `.` as matching a specific character or stopping at the first match found without exploring other branches. For example, searching ".ad" in a Trie with "bad" and "dad" should try both the 'b' and 'd' branches. If you only try one and it fails, you might incorrectly return `False`. wrong_approach: "Match '.' to a single arbitrary character" correct_approach: "Use DFS to try all children when encountering '.'" - title: Forgetting the Word-End Check description: | The Trie might contain "bad" but not "ba". If you search for "ba", you'll successfully traverse 'b' → 'a', but that doesn't mean "ba" is a stored word. Always check `is_end` at the final node. A successful traversal only counts if the final node marks a complete word. wrong_approach: "Return True if traversal completes" correct_approach: "Return node.is_end to verify word existence" key_takeaways: - "**Trie fundamentals**: A Trie stores strings character-by-character, enabling efficient prefix operations and exact lookups in O(m) time" - "**Wildcard + DFS**: When a pattern can match multiple characters, use DFS to explore all possibilities — this pattern appears in regex matching, file globbing, and game AI" - "**Space-time tradeoff**: Tries use more memory than a simple list, but provide much faster search operations, especially for prefix-based queries" - "**Design pattern**: This problem demonstrates the classic pattern of combining a specialised data structure (Trie) with a traversal algorithm (DFS) to solve complex matching problems" time_complexity: "O(m) for `addWord` where `m` is the word length. O(26^k × m) worst case for `search` where `k` is the number of wildcards, though typically much faster due to pruning." space_complexity: "O(n × m) where `n` is the number of words and `m` is the average word length, to store all characters in the Trie." solutions: - approach_name: Trie with DFS is_optimal: true code: | class TrieNode: def __init__(self): # Maps character to child TrieNode self.children: dict[str, 'TrieNode'] = {} # True if a complete word ends at this node self.is_end = False class WordDictionary: def __init__(self): # Root node of the Trie self.root = TrieNode() def addWord(self, word: str) -> None: # Start at root and traverse/create nodes for each char node = self.root for char in word: # Create child node if it doesn't exist if char not in node.children: node.children[char] = TrieNode() # Move to child node node = node.children[char] # Mark the end of a complete word node.is_end = True def search(self, word: str) -> bool: def dfs(index: int, node: TrieNode) -> bool: # Base case: reached end of search word if index == len(word): return node.is_end char = word[index] if char == '.': # Wildcard: try ALL children for child in node.children.values(): if dfs(index + 1, child): return True return False else: # Literal character: must match exactly if char not in node.children: return False return dfs(index + 1, node.children[char]) # Start search from root at index 0 return dfs(0, self.root) explanation: | **Time Complexity:** - `addWord`: O(m) where `m` is the word length — we traverse/create one node per character. - `search`: O(m) for exact matches. O(26^k × m) worst case when there are `k` wildcards, as each `.` can branch into up to 26 children. In practice, the Trie structure prunes many branches. **Space Complexity:** O(n × m) for `n` words of average length `m`. Each character potentially creates a new node, though shared prefixes reduce actual usage. The Trie enables efficient prefix-based storage, and DFS handles wildcard matching by exploring all valid paths. - approach_name: Brute Force with List is_optimal: false code: | class WordDictionary: def __init__(self): # Store all words in a list self.words: list[str] = [] def addWord(self, word: str) -> None: # Simply append to the list self.words.append(word) def search(self, word: str) -> bool: def matches(pattern: str, candidate: str) -> bool: # Length must match if len(pattern) != len(candidate): return False # Check character by character for p, c in zip(pattern, candidate): # '.' matches anything, otherwise must be equal if p != '.' and p != c: return False return True # Check every word in the list for stored_word in self.words: if matches(word, stored_word): return True return False explanation: | **Time Complexity:** - `addWord`: O(1) — just append to list. - `search`: O(n × m) where `n` is the number of stored words and `m` is the word length. We must check every word against the pattern. **Space Complexity:** O(n × m) to store all words. This approach is simpler but becomes slow with many words. For `10^4` operations, the Trie approach significantly outperforms this brute force solution, especially for repeated searches.