Skip to content
Bill Liao
Go back

Add and Search Word — Data structure design

Edit page

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

Implement the WordDictionary class:

Example:

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 wordDictionary.search(“b..”); // return True

Constraints:

Approach: Trie with DFS for Wildcards

Algorithm

  1. Store words in a trie; each node has 26 children and an isEnd flag
  2. addWord: insert each character along the trie, marking the last node as end
  3. search: traverse recursively; when the current character is '.', try every child branch; otherwise follow the specific child. Return true if a full word exists for some branch

Time & Space Complexity

Java Implementation

public class WordDictionary {

    private static class TrieNode {
        TrieNode[] children = new TrieNode[26];
        boolean isEnd;
    }

    private final TrieNode root;

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

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

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

    private boolean search(TrieNode node, String word, int pos) {
        if (node == null) {
            return false;
        }
        if (pos == word.length()) {
            return node.isEnd;
        }

        char c = word.charAt(pos);
        if (c == '.') {
            for (TrieNode child : node.children) {
                if (search(child, word, pos + 1)) {
                    return true;
                }
            }
            return false;
        }

        return search(node.children[c - 'a'], word, pos + 1);
    }

    // Test method
    public static void main(String[] args) {
        WordDictionary wordDictionary = new WordDictionary();
        wordDictionary.addWord("bad");
        wordDictionary.addWord("dad");
        wordDictionary.addWord("mad");
        System.out.println(wordDictionary.search("pad")); // return false
        System.out.println(wordDictionary.search("bad")); // return true
        System.out.println(wordDictionary.search(".ad")); // return true
        System.out.println(wordDictionary.search("b..")); // return true
    }
}

Example Walkthrough

After adding “bad”, “dad”, “mad”:

  1. search("pad"): the p child of the root does not exist -> false
  2. search("bad"): follows b -> a -> d, final node isEnd = true -> true
  3. search(".ad"): try all 26 children of root; only b, d, m branches exist, and b.ad, d.ad, m.ad all match -> true
  4. search("b.."): at the second position try all children of node b, eventually “bad” matches -> true

Key Points

  1. Trie Storage: Prefix sharing keeps the dictionary compact and enables character-by-character matching
  2. Wildcard Branching: A '.' forces recursion over all 26 children
  3. Recursive Search: The search index tracks progress instead of slicing strings
  4. End Marker: isEnd distinguishes complete words from prefixes
  5. Bounded Dots: The constraint of at most 2 dots keeps the exponential factor small

Edit page
Share this post:

Previous Post
Ugly Number II
Next Post
Implement Magic Dictionary