Design Add and Search Words Data Structure (LC 211): addWord(word) and
search(pattern), where the pattern may contain dots, each matching any one
letter. The brief’s sentence is the whole problem — every ordinary letter
narrows you to exactly one child and a dot refuses to, so the question is what
the lookup has to become once it can no longer be a single walk down the tree
— and the piece draws exactly that. The trie sits in the middle: one circle per
node, its letter inside, a violet outer ring on every node a word ends at.
The pattern sits top right as boxes, dots amber, each box filling in as the
search consumes it.
Then the search runs, one node per frame. A letter follows the one matching edge, teal. A dot draws an amber halo on every child at once — the fan — and the depth-first search takes them in order, so what you watch is a single live path (teal, root to cursor) that dives, dies, backs up to the last fan and dives again. A branch dies in one of three ways, each said out loud in the story line: the letter it needs has no child, a dot arrives at a leaf, or the pattern runs out on a node where no word ends. A match turns the path green and, by default, returns straight up the stack; every other live branch is abandoned unvisited.
Under the tree, touched is the count of nodes the search has entered so
far, drawn as a bar against a fence at L + 1 — what a dot-free walk of the
same length could touch at most. On "bad" the bar stops at the fence. On
".a." over seven words it goes well past it, and the story at the end says
by how much, and why LC 211 caps the dots at two.
Two switches.
match: any node at the right depth is the classic bug. Reaching a node
when the pattern runs out is not the same as a word ending there;
search("ba") after addWord("bad") comes back true, because "ba" is a
prefix of something. The path lights green on a node with no violet ring, the
readout says nothing matches, and nothing else about the search changes —
which is why the bug survives every test that only searches whole words.
stop: never removes the early return and counts every match instead of stopping at the first. That is the cost of the same pattern in the LC 212 / autocomplete shape, where you want all the hits, and it is how to see the whole fan for a two-dot pattern rather than the first branch that happens to succeed.
Type your own dictionary (up to twelve words) and pattern (up to eight characters), step through one node at a time, or let it run.
Reuse
src/wildcard.js is a framework-free ES module:
parseWords(text, { max, maxLen }),parsePattern(text, { maxLen, maxDots })— inputs out of free text, capped.buildTrie(words)— nodes as a flat array (id,ch,depth,end,parent,children), root at 0.search(trie, pattern)— the plain recursive answer, as you would write it.matches(words, pattern)— every matching word, the boring way; the oracle.simulate(words, pattern, { end, order })— runs one search and returnsframes(one per event —enter,fan,dead,match,done— each with a snapshot of the live stack, the touched set, the dead set and the hits), theresult, theexpectedmatches,touchedagainstsingle(L + 1), andok.endis'flag'or'node';orderis'first'or'all'. Same trace-per-tick shape asweave,relink,pruneandledger.layout(trie)— x by leaf order in[0, 1], y by depth;childIds(node)is the alphabetical order both the fan and the layout use, so they agree.
No rendering or timers in the module; the canvas demo is reference code.
Gotchas
- The bug switch only changes what a match is; the walk is identical. So on
any pattern that happens to end on a real word the two modes are
indistinguishable —
"ba","b.",".a"are the inputs that separate them. - With the early return on, the touched count depends on child order: the
fan goes alphabetically, so
".ad"findsbadon the first branch and".at"has to burybad/barfirst. Switch the stop to never to see the order-independent cost. - The demo bundles its own copy of
wildcard.js(self-contained by contract); re-copy after editingsrc/.