Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Trie Wildcard Matching

Welcome back! In our last couple of lessons, we've explored the Trie data structure, first learning its basic operations and then applying it to optimize a complex backtracking problem in Word Search II. You saw how a Trie can efficiently prune search paths on a 2D grid.

Today, we'll continue our deep dive into Tries by adding another powerful capability: wildcard matching. You will learn how to modify the Trie's search operation to handle queries containing a special . character, which can match any single letter. This is a classic interview problem that beautifully demonstrates how a simple recursive search within the Trie can solve a seemingly complex requirement.

From a Single Path to Many Paths

In a standard Trie search for a word like "cat", the path is deterministic. You start at the root, follow the edge for 'c', then 'a', then 't'. If any edge is missing, the word doesn't exist.

But what if the search query is ".ad"? The . character introduces ambiguity. It could be 'b', 'd', 'm', or any other letter. A standard traversal won't work. Instead of following a single path, we must be prepared to explore multiple paths simultaneously. This is where recursion, a pattern you are well-versed in from our backtracking lessons, becomes essential.

This Trie contains "bad", "dad", and "mad". To match ".ad", a search must explore the branches starting with 'b', 'd', and 'm'. For "m..", it would follow the 'm' branch and then explore all children of the 'a' node.

The Recursive Search (DFS) Strategy

The problem of exploring multiple branching paths is perfectly suited for a Depth-First Search (DFS) algorithm. We can define a recursive helper function that searches the Trie for the remainder of a word, starting from a given node.

The state of our recursive function, let's call it dfs, will need two pieces of information:

  1. node: The current TrieNode we are at in our traversal.
  2. index: The index of the character in the search word we are currently trying to match.

The logic inside the dfs(node, index) function will handle two main cases:

  • Base Case: If we have successfully matched all characters in the word (index == len(word)), our search is successful only if the current node marks the end of a word (node.is_end).

  • Recursive Step: We look at the character word[index]:

    • If it's a normal letter (e.g., 'a'): We behave like a standard Trie search. We check if the corresponding child exists. If it does, we continue the search from that child: dfs(node.children['a'], index + 1). If not, this path has failed.
    • If it's a wildcard .: This is the key step. We cannot follow a single path. We must iterate through all of the current node's children. For every child that exists, we recursively call the search function: dfs(child, index + 1). If any of these recursive calls returns True, we've found a valid match, and we can stop and return True. If we explore all children and none lead to a match, then this path fails.

This branching logic is the heart of wildcard matching in a Trie.

Implementation in Python

Let's look at a complete implementation. The following resource provides a clean Python solution that encapsulates this recursive logic within the search method.

Leetcode #211: Design Add and Search Words ... - Stealth Interview

This resource from Stealth Interview provides a full Python implementation of the WordDictionary class, including the Trie node definition, the addWord method, and the recursive search method.

Focus on the WordDictionary class and its methods. Read through the addWord method to understand how the Trie is built. Then, carefully study the search method and its nested recursive helper function. Pay close attention to the if c == '.' block, as this is where the wildcard logic is implemented.

The code you just reviewed implements the DFS strategy we discussed. The main search method is a wrapper that kicks off the recursive search from the root of the trie (self.trie) starting at the first character of the word.

Notice that the provided implementation uses slicing (word[i + 1:]) in its recursive calls. While conceptually clear, this creates a new copy of the substring at each step, which can be inefficient for long words. A more optimized approach, similar to what you saw in the JavaScript example below, passes an index idx that is simply incremented (idx + 1), avoiding the overhead of string slicing. This is a subtle but important implementation detail to be aware of for interview settings.

For a slightly different perspective, here is a JavaScript implementation that uses an index instead of slicing.

Leetcode - 211. Design Add and Search Words Data Structure

This article from dev.to shows a JavaScript version of the same logic.

Examine the search function and its nested dfs(node, idx) helper. Note how it handles the two cases for ch (wildcard vs. normal letter) and passes an incremented index idx + 1 in the recursive calls. This demonstrates the index-based approach to tracking progress.

Complexity Analysis

Understanding the performance implications of this algorithm is crucial. Your experience with algorithmic analysis will be helpful here.

  • addWord: Inserting a word of length takes time, as we just traverse or create one node per character.
  • search:
    • Without wildcards: The search is a simple traversal, taking time.
    • With wildcards: The complexity changes dramatically. In the worst-case scenario, such as searching for "..." in a Trie where every node has many children, the search branches at every level. If our alphabet has characters (e.g., 26), the time complexity can be up to . This exponential complexity arises because each . could potentially force the algorithm to explore all children. In practice, the branching is usually much more limited by the actual words in the Trie.

The following resource confirms this analysis.

Leetcode - 211. Design Add and Search Words Data Structure

This section provides a concise breakdown of the time complexity for both addWord and search operations.

Read the short section on complexity. Pay special attention to the explanation for the worst-case search time.

Conclusion

In this lesson, you have extended your knowledge of Tries to handle a powerful new feature: wildcard matching. This required moving from a simple iterative traversal to a more dynamic recursive search that can explore multiple paths.

Key Takeaways:

  • Wildcard characters like . transform a Trie search from a deterministic, single-path traversal into a multi-path exploration problem.
  • Depth-First Search (DFS), implemented recursively, is the natural pattern to solve this, mirroring the backtracking techniques you've already mastered.
  • The core logic of the recursive search involves a simple conditional: if the character is normal, follow one path; if it's a wildcard, branch and explore all possible child paths.
  • While powerful, wildcard searches have a worst-case exponential time complexity, a critical trade-off to understand.

We have now covered Tries in depth, from their basic structure to their application in complex search and matching scenarios. In our next module, we will switch to a completely different but equally powerful data structure: the Union-Find (or Disjoint Set Union). This structure is exceptionally efficient for solving problems related to connectivity, partitioning, and dynamic grouping.

Can't find a good explanation? Sign up and we'll make it for you

Sign up