150NeetCode 150

62.Design Add and Search Words Data Structure

MediumBlind 75Tries · 2/3

Problem

Design a data structure that supports adding words and checking whether a string matches any previously added word. Implement WordDictionary:

  • WordDictionary() initializes the object.
  • void addWord(String word) adds word.
  • boolean search(String word) returns true if some added word matches word. word may contain dots '.', and a dot matches any single letter.

A match must have the same length as the stored word.

Examples

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: ".ad" matches bad/dad/mad; "b.." matches bad.
Input:  ["WordDictionary","addWord","search","search"]
        [[],["a"],["."],["a."]]
Output: [null,null,true,false]
Explanation: "." matches "a"; "a." needs a 2-letter word.

Constraints

  • 1 <= word.length <= 25
  • word in addWord consists of lowercase English letters.
  • word in search consists of '.' or lowercase English letters.
  • There are at most 2 dots in word for search queries.
  • At most 10^4 calls in total to addWord and search.

💡 Key Insight

Store words in a trie. A normal letter follows exactly one child; a '.' must try every existing child, which turns search into a DFS over the trie. Because tries share prefixes and queries contain at most 2 dots, the branching stays small and only paths that actually exist are explored.

Brute Force (List of Words)

TimeO(1) for addWord; O(n · L) for searchSpaceO(total characters added).

Intuition

Keep every word in a list. For a query, compare it against each stored word character by character, treating '.' as a wildcard. Simple, but each query touches every stored word.

Algorithm

  1. addWord: append to words.
  2. search: for each stored w, skip if lengths differ; otherwise compare each position — a mismatch only counts when the query char is not '.'.
  3. Return true on the first full match, else false.

Visualization

words = [bad, dad, mad]     query = ".ad"

bad: . vs b ok, a=a, d=d  -> match -> true

Code

java
import java.util.ArrayList;
import java.util.List;

class WordDictionary {
    private final List<String> words = new ArrayList<>();

    public WordDictionary() {
    }

    public void addWord(String word) {
        words.add(word);
    }

    public boolean search(String word) {
        for (String w : words) {
            if (matches(w, word)) {
                return true;
            }
        }
        return false;
    }

    private boolean matches(String stored, String pattern) {
        if (stored.length() != pattern.length()) {
            return false;
        }
        for (int i = 0; i < pattern.length(); i++) {
            char p = pattern.charAt(i);
            if (p != '.' && p != stored.charAt(i)) {
                return false;
            }
        }
        return true;
    }
}

Complexity

  • Time: O(1) for addWord; O(n · L) for search — n stored words, L characters each.
  • Space: O(total characters added).

Complexity Comparison

Approach addWord search Space
List of Words O(1) O(n · L) O(total chars)
Bucket by Length O(L) O(k · L) O(total chars)
Trie + DFS O(L) O(L) to O(26^d · L) O(nodes · 26)

Common Pitfalls

  • Returning true when the pattern is consumed without checking isEnd — "ba" must not match after adding only "bad".
  • For '.', returning the result of the first child's DFS instead of trying all children until one succeeds.
  • Treating '.' as matching zero or many characters — it matches exactly one letter.
  • Computing '.' - 'a' as an index (negative) when the wildcard branch is not handled first.