3.4.14 · D4Trees

Exercises — Trie (prefix tree) — insert, search, startsWith, applications (autocomplete, spell check)

2,766 words13 min readBack to topic

We use these letters everywhere below, all defined once:


Level 1 — Recognition

(Can you read a trie and recall what each part means?)

Look at the reference trie built from the four words {car, card, care, dog}. This is the exact set from the parent note.

Figure — Trie (prefix tree) — insert, search, startsWith, applications (autocomplete, spell check)

L1.1

Counting the root, how many nodes does the trie in the figure contain?

Recall Solution L1.1

Walk every distinct path and count each circle once:

  • root — 1
  • c, a, r — 3 (the shared spine of car, card, care)
  • d (child of r, ending card) — 1
  • e (child of r, ending care) — 1
  • d, o, g (the dog branch) — 3

Total nodes. WHY the sharing matters: car, card, care cost only 3 spine nodes plus 2 branch tips, not separate letters. That collapse is the trie's benefit.

L1.2

How many nodes in the figure have isEnd = True?

Recall Solution L1.2

A word finishes at the last-letter node of each inserted word: car (the r), card (the d), care (the e), dog (the g). That is — exactly one per inserted word, always. The amber dots in the figure mark them.


Level 2 — Application

(Run the three operations by hand.)

Use the same trie {car, card, care, dog} unless a problem says otherwise.

L2.1

What does search("care") return, and why?

Recall Solution L2.1

Walk the letters: root c (exists) a (exists) r (exists) e (exists). We consumed all 4 characters with no broken step, landing on the e node. Now the deciding check: is e.isEnd True? Yes — care was inserted, so e is amber. Return .

L2.2

What does search("ca") return? Contrast it with startsWith("ca").

Recall Solution L2.2

search("ca"): root c a. Path exists, so we reach the a node. Final question: is a.isEnd True? No — we never inserted the word ca; a is just a passing point. Return . startsWith("ca"): same walk to a, but this operation ignores isEnd entirely and asks only "did the road survive?" The road survived, so return . The single-line difference (return node.isEnd vs return True) is the whole distinction.

L2.3

Starting from an empty trie, insert("cart"). Using the reference trie as the starting point instead, how many new nodes does insert("cart") create, and where?

Recall Solution L2.3

Walk cart on the reference trie: c (exists) a (exists) r (exists) then need child t of r. The r node currently has children d and e but no t. So create exactly 1 new node, the t under r, and set t.isEnd = True. Only new node — because car is a shared prefix already paved. The new node count equals the number of new characters, which is minus the length of the longest already-existing prefix ().


Level 3 — Analysis

(Reason about structure, cost, and edge cases.)

L3.1

You insert words, each of length exactly , and no two words share any prefix (every word starts with a different first letter). Excluding the root, how many nodes does the trie have? What is the node count including the root?

Recall Solution L3.1

No shared prefixes means no path is ever reused — each word gets its own private chain of nodes. So excluding the root: nodes. Including the root: . This is the trie's worst case for space: nothing is shared, so it degenerates into separate strands hanging off one root — no better than storing the strings separately, plus the overhead of node objects.

L3.2

Insert these words in order and give the final node count including root: a, ab, abc, abcd.

Recall Solution L3.2

Each word is a prefix of the next, so they walk the same single chain and only ever add one node at the tip:

  • a: create node a (mark end). Nodes: root, a .
  • ab: a exists, add b. .
  • abc: add c. .
  • abcd: add d. .

Final count including root . Notice all four of a, b, c, d are amber (isEnd) — four words, four flags, one chain. This is the best case for sharing: nested prefixes.

L3.3

On the trie of L3.2, how many nodes have isEnd = True, and how many are leaves? Explain the mismatch.

Recall Solution L3.3

isEnd nodes: a, b, c, d — that is . Leaves (nodes with no children): only d at the very tip — that is . Mismatch: 4 words but 1 leaf, because every word except the longest ends at an internal node. This is the crispest proof of why "leaf = word" fails and isEnd is mandatory.

L3.4

For a lookup query of length , why is search cost and not dependent on ? Compare against searching strings in a Binary Search Tree.

Recall Solution L3.4

search visits exactly one node per query character — at most steps — and each step is a single child lookup that does not care how many words are stored. So the time is , independent of : a million stored words don't slow one lookup. A Binary Search Tree of strings does node comparisons, but each comparison compares strings character by character, costing up to . Total . For large the trie's wins because it never re-pays the factor. (A Hash Table also gives membership but, as the parent stressed, cannot answer prefix queries at all.)


Level 4 — Synthesis

(Combine operations into a working feature.)

L4.1

Using the reference trie {car, card, care, dog}, hand-run autocomplete("car"). List the returned words in the order the DFS visits them if children are explored in alphabetical order.

Recall Solution L4.1

Walk the prefix car: root c a r. We stand on the r node. Now DFS the subtree, emitting a word whenever isEnd is True. Path accumulated so far is car.

  • r.isEnd is True emit car.
  • Explore children of r alphabetically: d before e.
    • Enter d: d.isEnd True emit card. d has no children.
    • Enter e: e.isEnd True emit care. e has no children.

Result: . Note dog never appears — the DFS started inside the relevant subtree, so it physically cannot reach unrelated branches. That pruning is why autocomplete is fast.

L4.2

Design (in pseudocode) a countWordsWithPrefix(prefix) that returns how many stored words start with prefix. Then evaluate it for prefix = "ca" on the reference trie.

Recall Solution L4.2

Idea: walk to the prefix node (like startsWith); if the walk breaks, return 0. Otherwise DFS the subtree and count every isEnd.

def countWordsWithPrefix(self, prefix):
    node = self.root
    for c in prefix:
        if c not in node.children:
            return 0            # road broke ⇒ zero words
        node = node.children[c]
    count = 0
    def dfs(n):
        nonlocal count
        if n.isEnd:
            count += 1
        for child in n.children.values():
            dfs(child)
    dfs(node)
    return count

For "ca": walk to the a node. DFS its subtree hits isEnd at r (car), d (card), e (care) = words. Evaluating countWordsWithPrefix("ca") = 3. (Production tip: store a running prefixCount integer in each node during insert to make this instead of .)

L4.3

A spell-checker flags misspellings using search. On a trie holding {cat, cart, care}, the user types cet. Show why search("cet") returns False, and outline how an Edit Distance budget of 1 finds the intended word.

Recall Solution L4.3

search("cet"): root c (exists) need child e of c. The c node's only child is a. Path breaks at character 2 return . So cet is flagged as misspelled. Suggestion via Edit Distance : allow one substitution/insertion/deletion while walking. Substituting the e for an a matches the existing a edge and continues cat reaching cat (isEnd True) with exactly 1 edit. So the suggestion is cat. The trie prunes the search: the moment a partial path exceeds the budget, its whole subtree is skipped — you never explore dog-like unrelated branches.


Level 5 — Mastery

(Delete, degenerate inputs, and design trade-offs.)

L5.1 (the delete pitfall)

Starting from a trie with only {car, card}, you delete card. Which nodes may be physically removed, and which must stay? Then, from that same starting trie, you delete car instead — what changes?

Recall Solution L5.1

Trie before: rootcar(isEnd, word car)d(isEnd, word card). Delete card: unset d.isEnd. Now d is a leaf with no isEnd and belongs to no word remove node d. Walk back up: r still has isEnd = True (it's the word car) stop, keep r, a, c, root. Only 1 node removed. Delete car (from the original two-word trie): unset r.isEnd. But r still has child d (for card) r is not prunable; keep it. Nothing is physically removed — you only cleared a flag. Rule: on delete, always just unset isEnd; prune a node only if it has no children and is not another word's end, walking upward until that condition fails.

L5.2 (degenerate inputs)

State the correct behaviour for each edge case: (a) search("") on any trie, (b) startsWith("") on any trie, (c) inserting the empty string "".

Recall Solution L5.2

(a) search(""): the loop over characters runs zero times, so node stays the root. Return root.isEnd. This is True only if the empty string was inserted, otherwise False. Answer: . (b) startsWith(""): zero-iteration loop leaves node at root; return True. Every trie "starts with" the empty prefix, always True. (c) insert(""): zero-iteration loop, then root.isEnd = True. It legitimately marks the empty string as a stored word — no new nodes created. This is exactly why the empty-string case is unambiguous and why search("") depends on whether this was done.

L5.3 (choosing the right structure)

For each requirement, name the best structure from {Hash Table, Binary Search Tree, plain Trie, Radix Tree (Compressed Trie), Suffix Tree}, with one-line justification. (a) Only membership checks, no prefix queries, minimal memory. (b) Prefix/autocomplete queries over millions of English words. (c) Same as (b) but memory is extremely tight and words share long non-branching tails like internationalization. (d) "Does pattern occur anywhere inside text (as a substring)?"

Recall Solution L5.3

(a) Hash Table membership, no tree overhead; but it cannot do prefixes, which is fine here. (b) plain Trie — lookups independent of , and prefix queries are its native strength. (c) Radix Tree (Compressed Trie) — merges long single-child chains into one edge, killing the per-character node overhead a plain trie suffers on words like internationalization. (d) Suffix Tree — built over all suffixes of , it answers "is a substring?" in ; a prefix trie of words cannot, since substrings aren't prefixes. (Related: Longest Prefix Match is the routing cousin of these prefix walks.)


Answer Key (quick check)

L1.1
9
L1.2
4
L2.1
True
L2.2
search False, startsWith True
L2.3
1 new node (the t under r)
L3.1
excluding root, including
L3.2
5 nodes including root
L3.3
4 isEnd, 1 leaf
L4.1
car, card, care
L4.2
3
L5.2
search("") = root.isEnd; startsWith("") = True