Design Add and Search Words Data Structure

Medium Tries

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

Input: addWord("bad"), addWord("dad"), addWord("mad"), search("pad"), search("bad"), search(".ad"), search("b..")
Output: [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

The key recognition signals are 'add words' and 'search with wildcard .', which indicate a Trie data structure with backtracking search. The Trie allows prefix sharing and fast traversal, while the '.' wildcard requires a DFS exploring all children at that position. This combination efficiently handles both exact and wildcard searches.

Optimal Approach

Implement a TrieNode class with a dictionary of children and an end_of_word flag. For addWord, insert characters sequentially, creating nodes as needed. For search, use a recursive DFS that, at each character, either follows the exact child node or, if '.', recursively explores all children. Return true if any path matches the entire word and ends at a node marked as end_of_word.

Time

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

1class TrieNode:
2 def __init__(self):
3 self.children = {}
4 self.end_of_word = False
5
6class 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

1

Build Trie Nodes and Insert Words to Share Prefixes

3self.children = {}
4self.end_of_word = False
8self.root = TrieNode()
11curr = self.root
12for c in word:
13 if c not in curr.children:
14 curr.children[c] = TrieNode()
15 curr = curr.children[c]
16curr.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.

2

Explore Trie Recursively to Match Words with Wildcards

19def 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
34return dfs(0, self.root)

Objective

To perform a depth-first search on the Trie that supports '.' wildcard matching by exploring all possible child nodes at wildcard positions.

Key Insight

The search function uses recursion to traverse the Trie from a given node and index. When encountering a '.', it recursively explores all children nodes, effectively branching the search. For normal characters, it follows the corresponding child node if it exists. This approach combines Trie traversal efficiency with backtracking to handle flexible pattern matching.

Interview Quick-Check

Core Logic

DFS explores all possible paths when encountering '.', ensuring all valid matches are found without prematurely pruning.

State & Boundaries

The recursion terminates when the end of the word is reached, returning whether the current node marks a complete word.

Common Pitfalls & Bugs

Returning False immediately when a character is not found prevents unnecessary exploration and ensures correctness.

Complexity

The recursive search may branch exponentially with the number of wildcards, but pruning and prefix sharing mitigate this in practice.

Line Analysis

This solution has 5 Critical lines interviewers watch for.

Line 16 Critical
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.

Line 23 Critical
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.

Line 25 Critical
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.

Line 29 Critical
if c not in curr.children:

Check if the character is not present among the current node's children.

This check is critical to avoid false positives; it ensures the search only continues along valid paths that exist in the Trie.

Line 32 Critical
return curr.end_of_word

Return whether the current node marks the end of a valid word after full traversal.

Returning the end_of_word flag here is essential to distinguish between partial matches and full word matches, ensuring search correctness.

Line 3 Core
self.children = {}

Initialize the children dictionary for the TrieNode.

This dictionary stores mappings from characters to child TrieNodes, enabling efficient traversal and insertion of words sharing prefixes.

Line 13 Core
if c not in curr.children:

Check if the current character is not already a child of the current node.

Detecting missing nodes triggers creation of new TrieNodes, expanding the Trie to accommodate new words.

Line 14 Core
curr.children[c] = TrieNode()

Create a new TrieNode for the missing character.

Adding a new node preserves the Trie structure and enables future searches to find this character path.

Line 24 Core
for child in curr.children.values():

Iterate over all child nodes of the current Trie node.

Exploring all children allows the search to consider every possible letter substitution for the wildcard.

Line 26 Core
return True

Return True immediately if any recursive path matches.

Early termination upon finding a valid match improves efficiency by pruning unnecessary exploration.

Line 27 Core
return False

Return False if no child paths match the wildcard.

This ensures the search correctly fails when no valid substitutions exist for the wildcard at this position.

Line 30 Core
return False

Return False immediately if the character is not found.

Early termination prevents unnecessary recursion and correctly signals no match for the current path.

Line 4 Core
self.end_of_word = False

Initialize the end_of_word flag to False.

This flag marks whether the current node corresponds to the end of a valid word, distinguishing prefixes from complete words.

Line 8 Core
self.root = TrieNode()

Create the root TrieNode for the WordDictionary.

The root node serves as the entry point for all word insertions and searches, representing an empty prefix.

Line 15 Core
curr = curr.children[c]

Move the current pointer to the child node corresponding to the character.

Advancing the pointer allows sequential insertion or traversal along the word's path.

Line 19 Core
def dfs(j, root):

Define the recursive DFS helper function for searching.

Encapsulating the search logic in a recursive function allows elegant handling of wildcards and branching.

Line 28 Core
else:

Handle the case where the character is not a wildcard.

Normal characters require exact matching in the Trie children to continue the search.

Line 31 Core
curr = curr.children[c]

Advance the current node pointer to the child node matching the character.

Moving forward in the Trie aligns the search with the word's characters, maintaining correctness.

Line 34 Core
return dfs(0, self.root)

Initiate the search from the root node starting at index 0.

This call triggers the recursive DFS search over the entire word from the root of the Trie.

Line 11 Standard
curr = self.root

Start traversal from the root node to add a new word.

Insertion begins at the root, progressing character by character to build or follow existing paths in the Trie.

Line 20 Standard
curr = root

Initialize the current node pointer for traversal.

Starting from the given root node ensures the search explores the correct subtree corresponding to the current recursion state.

Line 12 Standard
for c in word:

Iterate over each character in the word to insert.

Sequentially processing characters ensures the word is represented as a path in the Trie.

Line 21 Standard
for i in range(j, len(word)):

Iterate over the characters of the word starting from index j.

Processing characters sequentially aligns with the Trie structure and supports partial pattern matching.

Line 22 Standard
c = word[i]

Extract the current character from the word.

Isolating the character allows conditional logic to handle normal letters and wildcards distinctly.

Test Your Understanding

Why does the search function use DFS and explore all children when encountering a '.' character?

Because '.' can match any letter, the search must explore all possible child nodes at that position to find any valid word matching the pattern. This exhaustive exploration ensures no potential matches are missed.

Related Problems

Tries 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

or drill a free problem