← Back to DevBytes

Solving Design Add and Search Words in Python: Step-by-Step Guide

Design Add and Search Words in Python: Step-by-Step Guide

The "Design Add and Search Words" problem is a classic data structure challenge that frequently appears in coding interviews and real-world search applications. It asks you to design a system that supports adding new words and searching for existing words, with a special twist: the search function must support wildcard characters (typically the dot . character) that can match any letter. In this tutorial, we will walk through the problem, understand why it matters, build a solution from scratch using a Trie, and discuss best practices.

What Is the Problem?

The problem, popularized by LeetCode (Problem 211 - Design Add and Search Words Data Structure), requires you to implement a class WordDictionary with two main methods:

For example, after adding the words "bad", "dad", and "mad", a search for "pad" returns False, a search for "bad" returns True, and a search for ".ad" or "b.." also returns True because the dots match any character.

Why It Matters

This problem is important for several reasons. First, it teaches you how to design efficient data structures for string operations. Second, it introduces the Trie (prefix tree) data structure, which is foundational for autocomplete systems, spell checkers, and IP routing tables. Third, the wildcard search requirement forces you to think about tree traversal strategies, specifically depth-first search (DFS) with backtracking. Mastering this problem gives you a strong foundation for tackling more complex search and retrieval systems.

Understanding the Trie Data Structure

A Trie is a tree-like data structure where each node represents a character of a string. The root node is empty, and each path from the root to a node represents a prefix. Words are stored by traversing from the root, creating or following child nodes for each character. A special flag at a node indicates that a complete word ends there.

For the wildcard search, when we encounter a dot ., we must explore all possible child nodes at that level. This is where DFS comes into play: we recursively try every branch and return True if any path leads to a valid word match.

Step-by-Step Implementation

Let us build the solution step by step. We will create a TrieNode class and a WordDictionary class.

Step 1: Define the Trie Node

Each node needs a dictionary to store its children and a boolean flag to indicate whether it marks the end of a word.

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

Step 2: Initialize the Word Dictionary

The WordDictionary class will hold a root TrieNode as the starting point for all operations.

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

Step 3: Implement addWord

To add a word, we start at the root and iterate through each character. For each character, if it does not exist in the current node's children, we create a new TrieNode. We move to the child node and continue. At the end of the word, we mark the final node's is_end flag as True.

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

Step 4: Implement search with Wildcard Support

The search method is more complex because of the wildcard character. When we encounter a regular character, we simply follow the corresponding child node. When we encounter a dot, we must recursively search all children. We use a helper function that performs DFS.

def search(self, word: str) -> bool:
    def dfs(index, node):
        for i in range(index, len(word)):
            char = word[i]
            if char == '.':
                # Try all possible children
                for child in node.children.values():
                    if dfs(i + 1, child):
                        return True
                return False
            else:
                if char not in node.children:
                    return False
                node = node.children[char]
        return node.is_end

    return dfs(0, self.root)

Complete Solution

Here is the complete, runnable solution combining all the pieces:

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


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

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

    def search(self, word: str) -> bool:
        def dfs(index, node):
            for i in range(index, len(word)):
                char = word[i]
                if char == '.':
                    for child in node.children.values():
                        if dfs(i + 1, child):
                            return True
                    return False
                else:
                    if char not in node.children:
                        return False
                    node = node.children[char]
            return node.is_end

        return dfs(0, self.root)


# Example usage
if __name__ == "__main__":
    wd = WordDictionary()
    wd.addWord("bad")
    wd.addWord("dad")
    wd.addWord("mad")

    print(wd.search("pad"))   # False
    print(wd.search("bad"))   # True
    print(wd.search(".ad"))   # True
    print(wd.search("b.."))   # True
    print(wd.search("..."))   # True
    print(wd.search("b.d"))   # True
    print(wd.search("ba."))   # True
    print(wd.search("b..."))  # False

How to Use It

Using the WordDictionary class is straightforward. Instantiate the class, add words using addWord, and query using search. The search method accepts exact words and patterns with dots. Each dot represents exactly one character, similar to the single-character wildcard in regular expressions.

You can integrate this class into larger applications such as autocomplete systems, dictionary lookups, or games like crossword solvers where partial information is available.

Complexity Analysis

Understanding the time and space complexity helps you evaluate whether this solution fits your use case:

Best Practices

When implementing and using this data structure, keep the following best practices in mind:

Alternative Approach: Iterative Search with a Stack

If recursion depth is a concern, you can rewrite the search method using an explicit stack. This avoids potential RecursionError on very long words:

def search(self, word: str) -> bool:
    stack = [(0, self.root)]
    while stack:
        index, node = stack.pop()
        if index == len(word):
            if node.is_end:
                return True
            continue
        char = word[index]
        if char == '.':
            for child in node.children.values():
                stack.append((index + 1, child))
        else:
            if char in node.children:
                stack.append((index + 1, node.children[char]))
    return False

This iterative version produces the same results but uses explicit stack management, making it safer for extremely long inputs.

Conclusion

The "Design Add and Search Words" problem is an excellent exercise in combining the Trie data structure with recursive search techniques. By building a Trie to store words efficiently and using DFS to handle wildcard characters, you create a flexible and powerful search system. Whether you are preparing for coding interviews or building a real-world search feature, understanding this solution equips you with the skills to handle prefix-based and pattern-based string queries. Remember to consider your specific use case when choosing between recursive and iterative approaches, and always test edge cases to ensure robustness.

— Ad —

Google AdSense will appear here after approval

← Back to all articles