Skip to content
JZLeetCode
Go back

LeetCode 211 Design Add and Search Words Data Structure

Table of contents

Open Table of contents

Description

Question Link: LeetCode 211

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

Implement the WordDictionary class:

Example 1:

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
wordDictionary.search("bad"); // return True
wordDictionary.search(".ad"); // return True, matches bad, dad, mad
wordDictionary.search("b.."); // return True, matches bad

Constraints:

Solution 1: Trie + DFS

Idea

Build a standard trie (prefix tree). addWord inserts character by character. For search, when we encounter a literal character we follow the single matching child. When we encounter '.', we must try all existing children — this is the DFS branching step.

         root
        / | \
       b  d  m
       |  |  |
       a  a  a
       |  |  |
       d* d* d*    (* = isWord)

search("b.."):  b -> a -> [try all children] -> d* ✓
search(".ad"):  [try b,d,m] -> a -> d* ✓
search("pad"):  no child 'p' at root -> false

The '.' wildcard creates branching factor up to 26 at each dot position. With at most 3 dots (per constraints), the worst case is O(263⋅L)O(26^3 \cdot L) which is bounded.

Complexity: addWord Time O(L)O(L), Space O(L)O(L). search Time O(L)O(L) without dots, O(26D⋅L)O(26^D \cdot L) worst case with DD dots. Overall space O(N⋅L)O(N \cdot L) for NN words of average length LL.

Java

The Node class is shared with LeetCode 208 Implement Trie.

class Node {
    private static final int R = 26;
    boolean isWord;
    Node[] next;

    Node() {
        next = new Node[R];
    }
}

class WordDictionary { // 452 ms, 96 Mb.
    Node root;

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

    public void addWord(String word) { // O(L)
        Node cur = root;
        for (int i = 0; i < word.length(); i++) {
            int id = word.charAt(i) - 'a';
            if (cur.next[id] == null) cur.next[id] = new Node();
            cur = cur.next[id];
        }
        cur.isWord = true;
    }

    public boolean search(String word) { // O(L) avg, O(26^D * L) worst
        return match(word, 0, root);
    }

    private boolean match(String word, int d, Node n) {
        if (d == word.length()) return n.isWord;
        int id = word.charAt(d) - 'a';
        if (word.charAt(d) != '.') return n.next[id] != null && match(word, d + 1, n.next[id]);
        else {
            for (int i = 0; i < n.next.length; i++) // O(26) branching
                if (n.next[i] != null)
                    if (match(word, d + 1, n.next[i])) return true;
        }
        return false;
    }
}

Python

class WordDictionary:
    """Trie + DFS. addWord O(L), search O(L) avg, O(26^D * L) worst with wildcards. Space O(N*L)."""

    def __init__(self):
        self.children: dict[str, 'WordDictionary'] = {}
        self.is_word = False

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

    def search(self, word: str) -> bool:
        return self._match(word, 0)

    def _match(self, word: str, i: int) -> bool:
        if i == len(word):
            return self.is_word
        c = word[i]
        if c != '.':
            return c in self.children and self.children[c]._match(word, i + 1)
        return any(child._match(word, i + 1) for child in self.children.values())  # O(26) per dot

C++

class WordDictionary {
    struct TrieNode {
        TrieNode* children[26]{};
        bool isEnd = false;
        ~TrieNode() { for (auto& child : children) delete child; }
    };

    TrieNode* root;

    bool dfs(TrieNode* node, const string& word, int pos) {
        if (!node) return false;
        if (pos == (int)word.size()) return node->isEnd;
        char c = word[pos];
        if (c == '.') {
            for (auto& child : node->children) // O(26) branching at each dot
                if (dfs(child, word, pos + 1)) return true;
            return false;
        }
        return dfs(node->children[c - 'a'], word, pos + 1); // O(1) for exact match
    }

public:
    WordDictionary() : root(new TrieNode()) {}
    ~WordDictionary() { delete root; }

    void addWord(const string& word) { // O(L) time, O(L) space for new nodes
        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(const string& word) { // O(26^D * L) worst case
        return dfs(root, word, 0);
    }
};

Rust

use std::collections::HashMap;

#[derive(Default)]
struct TrieNode {
    children: HashMap<char, TrieNode>, // O(1) lookup per character
    is_end: bool,
}

struct WordDictionary {
    root: TrieNode,
}

impl WordDictionary {
    fn new() -> Self {
        WordDictionary { root: TrieNode::default() }
    }

    fn add_word(&mut self, word: String) { // O(L) time, O(L) space
        let mut node = &mut self.root;
        for ch in word.chars() {
            node = node.children.entry(ch).or_default();
        }
        node.is_end = true;
    }

    fn search(&self, word: String) -> bool { // O(26^D * L) worst case
        Self::dfs(&self.root, word.as_bytes(), 0)
    }

    fn dfs(node: &TrieNode, word: &[u8], i: usize) -> bool {
        if i == word.len() { return node.is_end; }
        let ch = word[i] as char;
        if ch == '.' {
            for child in node.children.values() { // O(26) branching
                if Self::dfs(child, word, i + 1) { return true; }
            }
            false
        } else {
            match node.children.get(&ch) { // O(1) exact match
                Some(child) => Self::dfs(child, word, i + 1),
                None => false,
            }
        }
    }
}

Solution 2: HashMap by Length

Idea

Group words by length in a hash map. To search, iterate all words of the same length and compare character by character, treating '.' as a wildcard. This avoids the trie data structure entirely but trades search efficiency for simplicity.

Complexity: addWord Time O(1)O(1) amortized. search Time O(N⋅L)O(N \cdot L) where NN is the number of stored words of the same length. Space O(N⋅L)O(N \cdot L).

Java

class WordDictionaryMap { // 2372ms, 50.5 Mb.
    Map<Integer, List<String>> map = new HashMap<>(); // space O(N*L)

    public void addWord(String word) { // O(1) amortized
        int len = word.length();
        if (!map.containsKey(len)) map.put(len, new ArrayList<>());
        map.get(len).add(word);
    }

    public boolean search(String word) { // O(N*L) worst case
        int len = word.length();
        if (!map.containsKey(len)) return false;
        for (String s : map.get(len))
            if (isSame(s, word)) return true;
        return false;
    }

    private boolean isSame(String s, String word) {
        for (int i = 0; i < word.length(); i++) // O(L), early exit on mismatch
            if (word.charAt(i) != '.' && word.charAt(i) != s.charAt(i)) return false;
        return true;
    }
}

Python

class WordDictionaryMap:
    """HashMap by length, brute-force match. addWord O(1), search O(N*L). Space O(N*L)."""

    def __init__(self):
        self.words: dict[int, list[str]] = {}

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

    def search(self, word: str) -> bool:
        for s in self.words.get(len(word), []):  # O(N) words of same length
            if all(wc == '.' or wc == sc for wc, sc in zip(word, s)):  # O(L)
                return True
        return False
Share this post on:

Previous Post
System Design - How Regex Engines Work
Next Post
LeetCode 974 Subarray Sums Divisible by K