Design Add and Search Words Data Structure

medium

Design a data structure that supports adding new words and searching for words with wildcard matching. Implement the WordDictionary class with the following methods:

  • addWord(word) — Adds word to the data structure so it can be matched later.
  • search(word) — Returns true if there is any word in the data structure that matches word. The character . acts as a wildcard that can match any single letter.

Examples

Example 1

  • Input: addWord("bad"), addWord("dad"), addWord("mad"), search("pad"), search("bad"), search(".ad"), search("b..")
  • Output: false, true, true, true
  • Explanation: "pad" was not added. "bad" matches exactly. ".ad" matches "bad", "dad", and "mad". "b.." matches "bad".

Example 2

  • Input: addWord("a"), addWord("ab"), search("a"), search("."), search("ab"), search(".b"), search("ab."), search(".")
  • Output: true, true, true, true, false, true

Constraints

  • 1 <= word.length <= 25
  • word in addWord consists of lowercase English letters.
  • word in search consists of lowercase English letters or ..
  • There will be at most 10^4 calls to addWord and search.

Approaches

2 worked solutions ranked from straightforward to optimal — each with intuition, algorithm, complexity, and verified code in C++, Java, Python, and Go.

Approach 1

Intuition

A trie (prefix tree) is ideal for storing words and performing prefix-based lookups. Each node has up to 26 children (one per letter) and a flag indicating whether a complete word ends at that node. Adding a word is a straightforward traversal, creating nodes as needed. Searching with wildcard support requires a depth-first search: for regular characters, we follow the corresponding child; for ., we try all existing children and succeed if any path matches the remaining pattern.

Algorithm

  1. **addWord:** Starting from the root, for each character create a child node if it does not exist, then move to that child. Mark the final node as a word ending.
  2. **search:** Implement a recursive DFS function taking the current node and position in the word.
  3. At each position, if the character is a letter, follow the corresponding child (return false if it does not exist).
  4. If the character is `.`, iterate over all non-null children and recursively search each one. Return true if any child path succeeds.
  5. When the end of the search word is reached, return whether the current node is marked as a word ending.
TimeO(m) for `addWord` where m is word length; O(26^m) worst case for `search` with all `.` characters, but typically much fasterSpaceO(n * m) where n is the number of words and m is average word length

Code (C++ · Java · Python · Go)

C++17
class WordDictionary {
    struct TrieNode {
        TrieNode* children[26] = {};
        bool isEnd = false;
    };

    TrieNode* root;

    bool dfs(TrieNode* node, const string& word, int index) {
        if (index == word.size()) return node->isEnd;

        char c = word[index];
        if (c == '.') {
            for (int i = 0; i < 26; i++) {
                if (node->children[i] && dfs(node->children[i], word, index + 1)) {
                    return true;
                }
            }
            return false;
        } else {
            int idx = c - 'a';
            if (!node->children[idx]) return false;
            return dfs(node->children[idx], word, index + 1);
        }
    }

public:
    WordDictionary() {
        root = new TrieNode();
    }

    void addWord(string word) {
        TrieNode* node = root;
        for (char c : word) {
            int idx = c - 'a';
            if (!node->children[idx]) {
                node->children[idx] = new TrieNode();
            }
            node = node->children[idx];
        }
        node->isEnd = true;
    }

    bool search(string word) {
        return dfs(root, word, 0);
    }
};
Java 21
class WordDictionary {
    private class TrieNode {
        TrieNode[] children = new TrieNode[26];
        boolean isEnd = false;
    }

    private TrieNode root;

    public WordDictionary() {
        root = new TrieNode();
    }

    public void addWord(String word) {
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            int idx = c - 'a';
            if (node.children[idx] == null) {
                node.children[idx] = new TrieNode();
            }
            node = node.children[idx];
        }
        node.isEnd = true;
    }

    public boolean search(String word) {
        return dfs(root, word, 0);
    }

    private boolean dfs(TrieNode node, String word, int index) {
        if (index == word.length()) return node.isEnd;

        char c = word.charAt(index);
        if (c == '.') {
            for (int i = 0; i < 26; i++) {
                if (node.children[i] != null && dfs(node.children[i], word, index + 1)) {
                    return true;
                }
            }
            return false;
        } else {
            int idx = c - 'a';
            if (node.children[idx] == null) return false;
            return dfs(node.children[idx], word, index + 1);
        }
    }
}
Python 3.12
class WordDictionary:
    def __init__(self):
        self.children = {}
        self.is_end = False

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

    def search(self, word: str) -> bool:
        def dfs(node: "WordDictionary", index: int) -> bool:
            if index == len(word):
                return node.is_end

            c = word[index]
            if c == ".":
                for child in node.children.values():
                    if dfs(child, index + 1):
                        return True
                return False
            else:
                if c not in node.children:
                    return False
                return dfs(node.children[c], index + 1)

        return dfs(self, 0)
Go 1.26
type WordDictionary struct {
    children [26]*WordDictionary
    isEnd    bool
}

func Constructor() WordDictionary {
    return WordDictionary{}
}

func (wd *WordDictionary) AddWord(word string) {
    node := wd
    for _, c := range word {
        idx := c - 'a'
        if node.children[idx] == nil {
            node.children[idx] = &WordDictionary{}
        }
        node = node.children[idx]
    }
    node.isEnd = true
}

func (wd *WordDictionary) Search(word string) bool {
    return wd.dfs(word, 0)
}

func (wd *WordDictionary) dfs(word string, index int) bool {
    if index == len(word) {
        return wd.isEnd
    }

    c := word[index]
    if c == '.' {
        for i := 0; i < 26; i++ {
            if wd.children[i] != nil && wd.children[i].dfs(word, index+1) {
                return true
            }
        }
        return false
    }

    idx := c - 'a'
    if wd.children[idx] == nil {
        return false
    }
    return wd.children[idx].dfs(word, index+1)
}
Approach 2

HashMap by Word Length

Intuition

Instead of a trie, we can group words by their length in a hash map. When searching, we only need to check words of the same length as the search pattern. For each candidate word, we compare character by character, treating . as a match for any character. This approach is simpler to implement but less efficient when many words share the same length.

Algorithm

  1. Maintain a hash map where keys are word lengths and values are lists of words with that length.
  2. **addWord:** Append the word to the list for its length.
  3. **search:** Get the list of words with the same length as the search pattern. If no list exists, return false.
  4. For each candidate word in the list, compare character by character with the pattern. If the pattern character is `.`, it matches any character. If all characters match, return true.
  5. If no word in the list matches, return false.
TimeO(m) for `addWord`; O(n * m) for `search` where n is the number of words of the same length and m is the word lengthSpaceO(n * m) for storing all words

Code (C++ · Java · Python · Go)

C++17
class WordDictionary {
    unordered_map<int, vector<string>> words;

public:
    WordDictionary() {}

    void addWord(string word) {
        words[word.size()].push_back(word);
    }

    bool search(string word) {
        int len = word.size();
        if (words.find(len) == words.end()) return false;

        for (const string& w : words[len]) {
            bool match = true;
            for (int i = 0; i < len; i++) {
                if (word[i] != '.' && word[i] != w[i]) {
                    match = false;
                    break;
                }
            }
            if (match) return true;
        }
        return false;
    }
};
Java 21
class WordDictionary {
    private Map<Integer, List<String>> words;

    public WordDictionary() {
        words = new HashMap<>();
    }

    public void addWord(String word) {
        words.computeIfAbsent(word.length(), k -> new ArrayList<>()).add(word);
    }

    public boolean search(String word) {
        int len = word.length();
        List<String> candidates = words.get(len);
        if (candidates == null) return false;

        for (String w : candidates) {
            boolean match = true;
            for (int i = 0; i < len; i++) {
                if (word.charAt(i) != '.' && word.charAt(i) != w.charAt(i)) {
                    match = false;
                    break;
                }
            }
            if (match) return true;
        }
        return false;
    }
}
Python 3.12
from collections import defaultdict


class WordDictionary:
    def __init__(self):
        self.words = defaultdict(list)

    def addWord(self, word: str) -> None:
        self.words[len(word)].append(word)

    def search(self, word: str) -> bool:
        for w in self.words[len(word)]:
            if all(wc == pc or pc == "." for wc, pc in zip(w, word)):
                return True
        return False
Go 1.26
type WordDictionary struct {
    words map[int][]string
}

func Constructor() WordDictionary {
    return WordDictionary{words: make(map[int][]string)}
}

func (wd *WordDictionary) AddWord(word string) {
    length := len(word)
    wd.words[length] = append(wd.words[length], word)
}

func (wd *WordDictionary) Search(word string) bool {
    length := len(word)
    for _, w := range wd.words[length] {
        match := true
        for i := 0; i < length; i++ {
            if word[i] != '.' && word[i] != w[i] {
                match = false
                break
            }
        }
        if match {
            return true
        }
    }
    return false
}

Frequently asked questions

What is the optimal time complexity of Design Add and Search Words Data Structure?

The most efficient approach in this guide ("HashMap by Word Length") runs in O(m) for `addWord`; O(n * m) for `search` where n is the number of words of the same length and m is the word length time and uses O(n * m) for storing all words extra space. Earlier brute-force approaches are slower; see all 2 approaches above for the full progression.

What pattern does Design Add and Search Words Data Structure use?

Design Add and Search Words Data Structure is a medium-level Tries problem involving Trie, String, Depth-First Search. Recognizing the pattern is half the work — see the intuition section above for how to spot it.

Can Design Add and Search Words Data Structure be solved without extra space?

The most space-efficient approach in this guide uses O(n * m) for storing all words extra space (excluding the input). If you're aiming for in-place — see the trade-off in the algorithm section above — the brute-force approach uses O(n * m) where n is the number of words and m is average word length, while the optimized version uses O(n * m) for storing all words.

Which programming languages does this Ratta solution support?

Every approach above ships with verified, runnable solutions in C++, Java, Python, and Go. The starter templates in the workspace match the same four languages so you can practice in your interview language of choice.

Related Problems

Original problem: leetcode.com

Loading editor...
operations=
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
arguments=
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
[null,null,null,null,false,true,true,true]