2Brute2Best
🔥0

Design Add and Search Words Data Structure

Medium·LeetCode #211Basic Anchor
LeetCode
30-Second Mental ModelTrie + DFS search (back up at each wildcard)
Complexity:O(N · L)→O(26^d · L)
Key Insight

Store every word in a trie: one node per letter, with words that share a start sharing the top of the path. Each node has up to 26 children, and isEnd is set only on the node where a word ends. search walks the pattern down the trie. A letter follows one child. A . tries every child in turn and backs up after each failure. It returns true only if it uses up the pattern on a node with isEnd set.

Core Validity Invariant

A search returns true only if it uses up the whole pattern and stops on a node with isEnd set. Each letter of the pattern follows a child that really exists, and each . is replaced by a real child.

01

Problem

Build a class WordDictionary that stores words and answers pattern queries.

  • WordDictionary() creates an empty dictionary.
  • addWord(word) stores a word. You can add words at any time.
  • search(pattern) returns true if some stored word matches the pattern, and false otherwise.

A pattern matches a word only when both have the same length and every letter agrees. The one exception is ., which stands for any single letter. So b.. matches bad, and ..d matches bad, dad and mad. A prefix of a stored word does not count: ba does not match, even though it is the start of bad.

Input

A sequence of calls: addWord(word) with lowercase letters, and search(pattern) with lowercase letters and ..

Output

For each search call, true or false. addWord returns nothing.

Example 1
Input: addWord("bad"), addWord("dad"), addWord("mad"), search("pad"), search("bad"), search(".ad"), search("b..")
Output: false, true, true, true

pad does not start any stored word. bad is stored. .ad matches bad, dad or mad. b.. matches bad.

Example 2
Input: addWord("bad"), search("ba"), search("...")
Output: false, true

ba is only the start of bad, and no word ends there, so it does not match. ... has three letters, so it matches bad.

Constraints
  • • 1 ≤ word length ≤ 25
  • • Words given to `addWord` use only lowercase letters `a` to `z`.
  • • Patterns given to `search` use lowercase letters and `.`.
  • • A pattern has at most 2 dots.
  • • At most 10^4 calls in total to `addWord` and `search`.
02

First, think yourself

Before opening any solution, try to answer these on paper. Use hints only when stuck — each hint is counted.

  1. 1.

    If the pattern has no dots, how would I check whether one stored word matches it, letter by letter?

  2. 2.

    A . can be any letter. If the pattern is .ad, how many different words could it be, and where do they split?

  3. 3.

    If I store bad and the pattern is ba, is ba a match? What decides whether a walk through the letters counts as a word?

  4. 4.

    bad and bat both start with ba. If I store them separately, do I compare b and a twice?

  5. 5.

    How many stored words would a plain list force me to check for one search if there are 10,000 of them?

03

Brute force approach

05

Optimized approach

06

Why does this algorithm work?

07

Dry run

08

All approaches compared

ApproachIdeaTimeSpace
Brute ForceKeep words in a list per length; compare the pattern letter by letter with eachO(N · L)O(T)
OptimalWords share a tree of letters; a plain letter follows one child, a dot tries every childO(26^d · L)O(T)
09

Why does this belong to this pattern?

Clues in the problem statement
  • “stores words to search later”Many words and many lookups → build one structure over the words, not a list to scan.
  • “`.` matches any one letter”A wildcard lets one position take several paths → a DFS that backs up at each dot.
  • “matches any previously added string”The whole word must match, so a walk that stops early on a prefix is not a match → keep an end flag on each node.
one node per letter+a letter follows one child+a dot tries every child (DFS)+true only on an end node→Trie + DFS search (back up at each wildcard)

A trie alone is not enough. Implement Trie's search follows a single path, because every letter is fixed. Here a . makes the walk branch, so the search must try each child and back up after a failure, the same way a grid DFS does. The end check matters just as much: the search must stop on a word end, not just on any node it reaches. Word Search II uses this trie-plus-DFS walk too, but over letters on a board.

10

Don't make this mistake

Returning true when the walk only reaches a prefix (pattern confusion)
✗ if (i == (int)pattern.size()) return true; // a prefix is not a stored word
✓ if (i == (int)pattern.size()) return node->isEnd;

This is the startsWith check from Implement Trie, not search. Dry run: bad is stored. search("ba") walks root → b → a and uses up the pattern, so the wrong line returns true. But ba was never added. Only the node where the walk stops, checked with isEnd, says whether a word ends there.

Setting isEnd on every node while adding
✗ for (char c : word) { ... node->isEnd = true; // marks every prefix too }
✓ // after the loop, once: node->isEnd = true;

With bad added this way, the nodes for b and ba also become word ends. Then search("b") returns true, even though only bad was added.

A dot that matches no letter
✗ if (c == '.') return dfs(node, pattern, i + 1); // the dot does not move down
✓ for (TrieNode* child : node->next) if (child && dfs(child, pattern, i + 1)) return true; return false;

A dot must use up exactly one letter by moving to a child. In the dry run, .at with bad and cat stored: if the dot does not move down, the walk looks for a at the root, finds no child, and returns false. But cat matches .at.

Trying only the first child of a dot
✗ for (TrieNode* child : node->next) if (child) return dfs(child, pattern, i + 1);
✓ for (TrieNode* child : node->next) if (child && dfs(child, pattern, i + 1)) return true;

In the dry run, the first child of the root is b. For .at, b → a has no t, so the wrong version returns false and never tries c, where cat is. A dot must try every child and back up after each failure.

11

Want to implement this?

Write it yourself on LeetCode, then come back and compare your code with the solutions above.

Open coding workspace
How did it go?
Beyond The LeetCode Problem

How this concept connects to System Design & C++ Systems

Bridge DSA with production engineering

Top engineering interviews test whether you understand how algorithms like Trie translate into production architectures and low-level memory mechanics.