โ† Blogs
May 16, 2026โ€ข

Leetcode 211 Design Add And Search Words Data Structure

ullas kunder
Ullas Kunder

Designer & Developer

Table of Contents ยท 9 sections
  1. ๐Ÿงฉ The Problem
  2. Example
  3. ๐Ÿ› ๏ธ Approach: Trie + Recursive DFS
  4. Why Simpler Wins in Python ๐Ÿ
  5. ๐Ÿ” Visualizing the Approach (Dry Run)
  6. ๐Ÿ’ป Solution
  7. โฑ๏ธ Complexity Analysis
  8. ๐Ÿง  How to Recognize This Pattern
  9. ๐ŸŽฏ Interview Tips

Designing a system that supports both exact matches and wildcard searching is a classic interview challenge. While a simple hash set works for exact matches, it fails the moment you introduce a . wildcard. ๐Ÿงฉ

The key to solving this efficiently lies in the Trie (Prefix Tree), combined with a touch of Depth-First Search (DFS) to handle the branching logic of wildcards. ๐Ÿš€

๐Ÿงฉ The Problem

Design a data structure that supports adding new words and finding if a string matches any previously added string.

word may contain dots '.' where dots can be matched with any letter.

Example

Input: ["WordDictionary","addWord","addWord","search","search"] Args: [[],["bad"],["dad"],["bad"],[".ad"]]

Output: [null,null,null,true,true]


๐Ÿ› ๏ธ Approach: Trie + Recursive DFS

To support prefix-based storage and wildcard matching, we use a Trie.

  1. Storage: Each node in the Trie represents a character.
  2. Search:
    • If the character is a letter (e.g., 'a'), follow that specific child.
    • If the character is a wildcard ('.'), we must try all possible children at that level.

Why Simpler Wins in Python ๐Ÿ

In Python, "clever" often loses to "simple." Our first instinct might be to create a TrieNode class. However, using plain dictionaries is significantly faster because CPython internals are highly optimized for hash maps. No object overhead means fewer operations per character.

๐Ÿ” Visualizing the Approach (Dry Run)

Let's look at how the Trie evolves with addWord("bad"), addWord("dad"), and then a search(".ad").

Step Operation Trie Structure (Simplified) Result
1 addWord("bad") root -> b -> a -> d -> $ Word stored
2 addWord("dad") root -> {b, d} -> a -> d -> $ Shared branches
3 search("pad") root has no p False
4 search(".ad") . tries b branch: a -> d -> $ True โœ…

๐Ÿ’ป Solution

This implementation uses a nested dictionary structure for the Trie, which achieves 97%+ performance on LeetCode.

class WordDictionary(object):
 
    def __init__(self):
        # A simple dict to store our Trie levels
        self.trie = {}
 
    def addWord(self, word):
        """
        :type word: str
        :rtype: None
        """
        node = self.trie
        for ch in word:
            if ch not in node:
                node[ch] = {}
            node = node[ch]
        node['$'] = True   # '$' marks the end of a valid word
 
    def search(self, word):
        """
        :type word: str
        :rtype: bool
        """
        return self._dfs(word, 0, self.trie)
 
    def _dfs(self, word, i, node):
        # Base Case: We've matched every character in the word
        if i == len(word):
            return '$' in node
        
        ch = word[i]
        
        if ch == '.':
            # Wildcard: Recurse into every possible child
            for child in node:
                if child != '$' and self._dfs(word, i + 1, node[child]):
                    return True
            return False
        
        # Exact Match: Follow the path or return False
        if ch not in node:
            return False
        return self._dfs(word, i + 1, node[ch])

โฑ๏ธ Complexity Analysis

Complexity Rating Description
Time (addWord) O(M)O(M)O(M) MMM is the length of the word being added.
Time (search) O(26kร—M)O(26^k \times M)O(26kร—M) kkk is the number of dots. Worst case: we branch 26 times per dot.
Space O(S)O(S)O(S) SSS is the total number of characters across all unique words.

[!NOTE] Since the constraints specify at most 2 dots per search, the 26k26^k26k branching factor is capped at 262=67626^2 = 676262=676, making it extremely efficient in practice.

๐Ÿง  How to Recognize This Pattern

Whenever you see:

  1. Prefix-based searching or "starting with" requirements.
  2. Wildcard/Regex-lite matching in a set of strings.
  3. Need for memory-efficient storage of large vocabularies.

Think: "Is this a Trie problem?"

This same pattern is used in:

  • Implement Trie (Prefix Tree)
  • Word Search II
  • Autocomplete systems.

๐ŸŽฏ Interview Tips

  1. Pythonic Optimization: Mention that you're using nested dictionaries instead of a TrieNode class to reduce object creation overhead. It shows you understand language-specific performance.
  2. The Base Case: Be careful with the DFS base case. Matching the length of the string isn't enough; you must also check if the current node is marked as the end of a word ('$').
  3. Read Constraints: Notice the "at most 2 dots" constraint. This tells you that a recursive DFS approach won't lead to a TLE (Time Limit Exceeded) because the branching factor is small. ๐Ÿ”„

โ† Previous

leetcode 11 container with most water

Next โ†’

graphics template