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

function_signature: "class WordDictionary: ..."

test_cases:
  visible:
    - input:
        operations: ["WordDictionary", "addWord", "addWord", "addWord", "search", "search", "search", "search"]
        arguments: [[], ["bad"], ["dad"], ["mad"], ["pad"], ["bad"], [".ad"], ["b.."]]
      expected: [null, null, null, null, false, true, true, true]
  hidden:
    - input:
        operations: ["WordDictionary", "addWord", "search", "search"]
        arguments: [[], ["a"], ["a"], ["."]]
      expected: [null, null, true, true]
    - input:
        operations: ["WordDictionary", "addWord", "addWord", "search", "search", "search"]
        arguments: [[], ["apple"], ["app"], ["app"], ["apple"], ["appl"]]
      expected: [null, null, null, true, true, false]
    - input:
        operations: ["WordDictionary", "search"]
        arguments: [[], ["a"]]
      expected: [null, false]
    - input:
        operations: ["WordDictionary", "addWord", "addWord", "search", "search"]
        arguments: [[], ["at"], ["and"], ["a"], [".at"]]
      expected: [null, null, null, false, false]
    - input:
        operations: ["WordDictionary", "addWord", "addWord", "addWord", "search", "search", "search"]
        arguments: [[], ["abc"], ["adc"], ["aec"], ["a.c"], ["..c"], ["..."]]
      expected: [null, null, null, null, true, true, true]
    - input:
        operations: ["WordDictionary", "addWord", "search", "search", "search"]
        arguments: [[], ["ab"], ["a."], [".b"], [".."]]
      expected: [null, null, true, true, true]

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

    &nbsp;

    **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`

    &nbsp;

    **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

    &nbsp;

    **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.
