Reasoning Into CodeArunank Divya Sharan · Python edition← Arunank’s portfolio
Module 18 · Tree BFS, Binary Search Trees, and Tries

180. Wildcard Word Dictionary

Interview · Download this solution · All solutions and shared helpers

Problem

Implement a word dictionary with addWord(word) and search(pattern), where '.' matches exactly one arbitrary character. Stored words contain no literal dot. All other characters match exactly. Empty words and patterns are supported; addWord returns None.

Examples

Example 1

run_operations(WordDictionary,[("addWord","bad"),("addWord","dad"),("search",".ad"),("search","b.")])
=> [None, None, True, False]

A dot consumes exactly one character and completion must be terminal.

Example 2

run_operations(WordDictionary,[("addWord",""),("search",""),("search",".")])
=> [None, True, False]

The empty word cannot supply a wildcard character.

Example 3

run_operations(WordDictionary,[("addWord","cat"),("search","c.t"),("search","dog")])
=> [None, True, False]

Literal edges still restrict wildcard searches.

Reveal the worked solution and reasoning

Why this mechanism works

Reuse the trie from kata 179. A literal pattern character chooses one edge, while a wildcard must try every child edge. The recursive state needs both the current trie node and the pattern index. Reaching the end of the pattern succeeds only at a terminal node, even after wildcards.

State and boundaries

match(node,i) asks whether some stored suffix below node matches pattern[i:]. The prefix before i has already matched.

Protocol

  1. Insert words using the trie.
  2. At pattern end return the terminal flag.
  3. On a literal follow its child if present.
  4. On a dot try every child at index i+1.
  5. Succeed if any branch completes.

Visualization

stored bad, dad, mad
pattern .ad -> branch b/d/m -> literal a -> literal d -> terminal
pattern b.  -> ends at nonterminal prefix -> False

Python implementation

from kata_179 import Trie
from structures import run_operations

class WordDictionary:
    def __init__(self):
        self.trie = Trie()

    def addWord(self, word):
        self.trie.insert(word)

    def search(self, pattern):
        def match(node, i):
            if i == len(pattern):
                return node.terminal
            char = pattern[i]
            if char == '.':
                return any(match(child, i + 1) for child in node.children.values())
            child = node.children.get(char)
            return child is not None and match(child, i + 1)
        return match(self.trie.root, 0)

Worked execution

Recorded state changes after statements; L numbers refer to the solution file. Long traces are explicitly condensed.

run_operations(WordDictionary,[("addWord","bad"),("addWord","dad"),("search",".ad"),("search","b.")])

L6 __init__: self.trie = Trie()
self=WordDictionary{'trie':Trie...}

L9 addWord: self.trie.insert(word)
self=WordDictionary{'trie':Trie...}; word='bad'

L9 addWord: self.trie.insert(word)
self=WordDictionary{'trie':Trie...}; word='dad'

L12 search: def match(node, i):
self=WordDictionary{'trie':Trie...}; pattern='.ad'

L13 match: if i == len(pattern):
node=TrieNode{'children':{...:..., ...:...}, 'terminal':False}; i=0; pattern='.ad'

L15 match: char = pattern[i]
node=TrieNode{'children':{...:..., ...:...}, 'terminal':False}; i=0; pattern='.ad'; char='.'

L13 match: if i == len(pattern):
node=TrieNode{'children':{...:...}, 'terminal':False}; i=1; pattern='.ad'

L15 match: char = pattern[i]
node=TrieNode{'children':{...:...}, 'terminal':False}; i=1; pattern='.ad'; char='a'

... 11 intermediate state changes omitted ...

L15 match: char = pattern[i]
node=TrieNode{'children':{...:...}, 'terminal':False}; i=1; pattern='b.'; char='.'

L13 match: if i == len(pattern):
node=TrieNode{'children':{...:...}, 'terminal':False}; i=2; pattern='b.'

L17 <genexpr>: return any(match(child, i + 1) for child in node.children.values())
.0=dict_valueiterator; child=TrieNode{'children':{...:...}, 'terminal':False}; i=1

L17 <genexpr>: return any(match(child, i + 1) for child in node.children.values())
.0=dict_valueiterator; child=TrieNode{'children':{...:...}, 'terminal':False}; i=1

Result: [None, None, True, False]. A dot consumes exactly one character and completion must be terminal.

run_operations(WordDictionary,[("addWord",""),("search",""),("search",".")])

L6 __init__: self.trie = Trie()
self=WordDictionary{'trie':Trie...}

L9 addWord: self.trie.insert(word)
self=WordDictionary{'trie':Trie...}; word=''

L12 search: def match(node, i):
self=WordDictionary{'trie':Trie...}; pattern=''

L13 match: if i == len(pattern):
node=TrieNode{'children':{}, 'terminal':True}; i=0; pattern=''

L12 search: def match(node, i):
self=WordDictionary{'trie':Trie...}; pattern='.'

L13 match: if i == len(pattern):
node=TrieNode{'children':{}, 'terminal':True}; i=0; pattern='.'

L15 match: char = pattern[i]
node=TrieNode{'children':{}, 'terminal':True}; i=0; pattern='.'; char='.'

L17 <genexpr>: return any(match(child, i + 1) for child in node.children.values())
.0=dict_valueiterator; i=0

Result: [None, True, False]. The empty word cannot supply a wildcard character.

Why the solution is correct

A literal transition matches precisely its required character. A wildcard transition enumerates all possible one-character matches and no zero- or multi-character matches. The terminal base enforces exact length and word completion. Induction on remaining pattern length proves the search result.

Time and space

Insertion takes expected O(L). Search visits at most O(V) relevant trie nodes; a coarse branching bound is O(b^L) for branching factor b and length L. O(L) recursive depth, plus shared trie storage.

Common mistakes

Returning true merely when the pattern ends accepts prefixes that are not words. Advancing the trie without advancing i makes a dot behave like an unbounded wildcard.

Reasoning card

State: match(node,i) asks whether some stored suffix below node matches pattern[i:]. The prefix before i has already matched.

Invariant: Recursive state is (trie node, pattern index).

Decision to rehearse: Normal characters choose one edge; wildcard branches across all children.

Finish: Succeed if any branch completes

Transfer exercise

Add '*' meaning any number of characters. Identify the new branching transitions and why memoizing (node,index) becomes useful.