211. Design Add And Search Words Data Structure - Explanation

Problem Link

Description

Design a data structure that supports adding new words and searching for existing words.

Implement the WordDictionary class:

  • void addWord(word) Adds word to the data structure.
  • 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.

Example 1:

Input:
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["day"],["bay"],["may"],["say"],["day"],[".ay"],["b.."]]

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

Explanation:
WordDictionary wordDictionary = new WordDictionary();
wordDictionary.addWord("day");
wordDictionary.addWord("bay");
wordDictionary.addWord("may");
wordDictionary.search("say"); // return false
wordDictionary.search("day"); // return true
wordDictionary.search(".ay"); // return true
wordDictionary.search("b.."); // return true

Constraints:

  • 1 <= word.length <= 20
  • 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,000 calls will be made to addWord and search.


Topics

Recommended Time & Space Complexity

You should aim for a solution with O(n) time for each function call and O(t + n) space, where n is the length of the string and t is the total number of nodes created in the Trie.


Hint 1

A brute force solution would be to store each added word in a list and search linearly through the list for a word every time. This would be an O(m * n) solution, where m is the size of the list and n is the length of the string. Can you think of a better way? Maybe there is a tree-like data structure.


Hint 2

We can use a Trie to implement adding and searching for words efficiently. Adding a word follows the standard Trie insertion process. However, when searching for a word containing '.', which can match any character, we need a different approach. Instead of directly matching, we consider all possible characters at the position of '.' and recursively check the rest of the word for each possibility. How would you implement it?


Hint 3

We traverse the word with index i, starting at the root of the Trie. For normal characters, we search as usual. When encountering a dot ('.'), we try all possible characters by recursively extending the search in each direction. If any path leads to a valid word, we return true; otherwise, we return false. Although we try all paths for a dot, the time complexity is still O(n) because there are at most two dots ('.') in the word, making the complexity O((26^2) * n).


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Trie (Prefix Tree) - A tree-like data structure used for efficient storage and retrieval of strings
  • Depth-First Search (DFS) - Used to traverse the Trie when encountering wildcard characters
  • Recursion - The DFS approach uses recursive calls to explore all possible character matches for wildcards

1. Brute Force

Intuition

The simplest way to solve this problem is to store all words as-is and check every stored word during search.

When searching:

  • If the lengths don't match → it can't be a match.
  • Compare characters one by one:
    • Exact match is required unless the search character is .
    • . acts as a wildcard and can match any character.

This approach works because the constraints are small enough, but it is not efficient for large datasets.

Algorithm

Data Structure

  • Maintain a list to store all added words.

addWord(word)

  1. Append the word to the list.

search(word)

  1. Loop through every stored word:
    • Skip if lengths differ.
  2. Compare characters index by index:
    • If characters match → continue.
    • If search character is . → treat as match.
    • Otherwise → stop checking this word.
  3. If all characters match → return true.
  4. If no word matches → return false.
class WordDictionary:

    def __init__(self):
        self.store = []

    def addWord(self, word: str) -> None:
        self.store.append(word)

    def search(self, word: str) -> bool:
        for w in self.store:
            if len(w) != len(word):
                continue
            i = 0
            while i < len(w):
                if w[i] == word[i] or word[i] == '.':
                    i += 1
                else:
                    break
            if i == len(w):
                return True
        return False

Time & Space Complexity

  • Time complexity: O(1)O(1) for addWord()addWord(), O(mn)O(m * n) for search()search().
  • Space complexity: O(mn)O(m * n)

Where mm is the number of words added and nn is the length of the string.


2. Depth First Search (Trie)

class TrieNode:
    def __init__(self):
        self.children = {}
        self.word = False


class WordDictionary:
    def __init__(self):
        self.root = TrieNode()

    def addWord(self, word: str) -> None:
        cur = self.root
        for c in word:
            if c not in cur.children:
                cur.children[c] = TrieNode()
            cur = cur.children[c]
        cur.word = True

    def search(self, word: str) -> bool:
        def dfs(j, root):
            cur = root

            for i in range(j, len(word)):
                c = word[i]
                if c == ".":
                    for child in cur.children.values():
                        if dfs(i + 1, child):
                            return True
                    return False
                else:
                    if c not in cur.children:
                        return False
                    cur = cur.children[c]
            return cur.word

        return dfs(0, self.root)

Time & Space Complexity

  • Time complexity: O(n)O(n) for addWord()addWord(), O(n)O(n) for search()search().
  • Space complexity: O(t+n)O(t + n)

Where nn is the length of the string and tt is the total number of TrieNodes created in the Trie.


Common Pitfalls

Returning True at Any Node Instead of Word-End Nodes

When the search reaches the end of the pattern, you must check if the current node marks the end of a valid word, not just that the node exists. A prefix match is not the same as a word match.

# Wrong - returns True for any prefix match
def dfs(j, root):
    # ... traversal logic ...
    return True  # Incorrect: "app" would match if "apple" was added

# Correct - check if current node is end of word
def dfs(j, root):
    # ... traversal logic ...
    return cur.word  # Only True if a complete word ends here

Not Exploring All Children for Wildcard

When encountering a . wildcard, you must try all possible children nodes. A common mistake is returning after the first child or not iterating through all 26 possible characters.

# Wrong - only checks first child
if c == ".":
    if cur.children and dfs(i + 1, list(cur.children.values())[0]):
        return True

# Correct - must try ALL children
if c == ".":
    for child in cur.children.values():
        if dfs(i + 1, child):
            return True
    return False

Confusing Node Existence with Word Existence

When a character is not found in the Trie, return False immediately. However, when using wildcards, absence of any children should also return False, but only after checking all possibilities.

# Pattern: handle missing paths correctly
if c == ".":
    for child in cur.children.values():
        if child is not None and dfs(i + 1, child):
            return True
    return False  # No valid path found
else:
    if c not in cur.children:
        return False  # Character path doesn't exist
    cur = cur.children[c]