Context:FIT2004_MOC Β· string retrieval in O(M) independent of N β a k-ary Tree branching on characters; the base structure that suffix tries and trees specialise
Parent Framework:Tree
Quick Revision
π― Objective: one child slot per alphabet character + one for the terminal $ β insert/search walk one edge per character βΉ O(M), and a $-first in-order DFS lists every word sorted in O(T).
π¦ Core Components:links = [None]*27 β O(1) child lookup | payload data β counts, frequencies, scores | iterative O(1) aux vs recursive O(M) aux.
β‘ Key Constraint:without $ a prefix is indistinguishable from a word β insert apple, and app βexistsβ. Every inserted word and every search key ends in $, and $ sits at links[0].
π How It Works
1. Structure β k+1 Children and the Terminal $
BST vs trie β a BST has 2 children (left/right); a trie has k β the charset size: a..z=26 Β· a..z+A..Z=52 Β· DNA =4 (A,C,G,T).
+1 for $ β apple$ means apple is stored; app reaching a node with no $ child is only a prefix. Searching tar in {taco, taro, tarot, coco, chobo} walks t-a-r successfully and fails on $ βΉ not found.
$ at links[0] β $ sorts before every letter, so a $-first in-order walk emits app before apple β consistent with sorting. Map aβ1, zβ26.
Fixed-size array, not a list/dict β index = character βΉ child lookup is O(k)=O(1) for a constant charset. Cost: the slots are mostly None βΉ wasted space.
Payload β whatever the query needs (frequency, top-k words, prefix count) β keep it small, since there is one per node and a trie has many nodes.
Edge vs node labels β the character properly lives on the edge (consistent with graph representation); labels drawn in nodes are also accepted in the exam.
2. Operations β Every Traversal Starts at the Root
Insert β for each character of word$: child exists βΉ move to it Β· missing βΉ create it, then move.
Search β for each character of key$: child exists βΉ move Β· missing βΉ return False / raise. Worst O(M), best O(1) (first character absent).
Full traversal β in-order DFS over links[0..26] visits every node once βΉ the stored words in alphabetical order in O(#nodes)=O(T) (T= total characters incl. $); with A words of length β€B that is O(AB).
Iterative vs recursive β same O(M) time; iterative is O(1) aux, recursive O(M) aux (stack depth = key length). Recursion is preferred when information must flow back up (subtree counts, best leaf below, flags) β iteration needs a second loop or an explicit stack.
Delete β not required at this level.
3. Properties β βUse the Properties to Explain Complexityβ
N words, longest M letters, T total characters including every $.
Nodes β β€N(M+1) βΉ O(NM); tighter β€T+1 βΉ O(T) (root + one node per character, when no prefixes are shared).
Leaves β β€N β every leaf is a $ node. Maximum, because duplicates end at the same $ leaf.
Lecture example β {taco, taro, tarot, coco, chobo}: T=27, 21 nodes (shared ta, tar, c), 5 leaves, height 6 (tarot$).
Prep P1 β {cat, cathode, canopy, dog, danger, domain}: T=37, 30 nodes, 24 inner, 6 leaves, height 8 (cathode$). The t node branches into $ and h β a stored word that is also a prefix keeps its own $ child.
4. Augmented Tries β Applied P1, P2, P5, P7
P1 distinct strings in O(T) β insert all with $, count $ nodes; equal strings end at the same node.
P2 count words with prefix p in O(m) β a counter in every node, +1 on every node moved to or created during insert βΉ node.data= number of words through it; query = walk p, return the count. Build cost unchanged. Rejected: walk p then count leaves below βΉ up to O(T).
P7 most powerful prefix in O(T) β add wiβΓdepth to each node on insert; answer = max-score node. baby10, bank20, bit40 βΉ score(b)=70, score(ba)=60, score(bit)=120.
P5 predecessor in O(m+n)(output-sensitive) β walk the query, remembering the deepest node with a child smaller than the next character; on success return the query; on failure jump back there, take its greatest smaller child, then follow the greatest child to a leaf. No such node βΉ null β trace below.
5. A Trie as an Accelerator [D] β Applied P10βP12
P10 max xβaiβ in O(w) β store the w-bit integers MSB-first in a binary trie; at each level prefer the child with the opposite bit to x (a 1 in a high position beats every lower bit), else take the only child.
P11 max subarray XOR in O(nw) β F(L,R)=aLβββ―βaRβ=PRββPLβ1β with prefix XOR P (xβx=0, associativity); for each R query the trie of P0β..PRβ1β with PRβ, then insert PRβ βΉ same move as Maximum Subarray Sumβs prefix sums.
P12 word break O(n2m)βO(n2+nm) β the Dynamic Programming loop βevery wβL that is a prefix of S[i..n]β is one trie walk of S[i..n]; each node with a $ child is a word p βΉ candidate DP[i+β£pβ£]. Build O(nm)+n walks of O(n).
class Node: def __init__(self, data=None): self.data = data # payload: count, frequency, ... self.links = [None] * 27 # [0] = '$', [1..26] = 'a'..'z'class Trie: def __init__(self): self.root = Node() @staticmethod def _index(c): return 0 if c == '$' else ord(c) - 97 + 1 def insert(self, word): # iterative: O(M) time, O(1) aux node = self.root for c in word + '$': i = self._index(c) if node.links[i] is None: # missing -> create node.links[i] = Node() node = node.links[i] # exists -> follow node.data = word # payload at the $ leaf def search(self, word): # O(M) worst, O(1) best node = self.root for c in word + '$': # the '$' step separates "tar" from "tarot" node = node.links[self._index(c)] if node is None: return False return True def insert_count(self, word): # recursive: O(M) time, O(M) aux self._insert_count(self.root, word + '$', 0) def _insert_count(self, node, key, k): node.data = (node.data or 0) + 1 # words passing through this node if k == len(key): return i = self._index(key[k]) if node.links[i] is None: node.links[i] = Node() self._insert_count(node.links[i], key, k + 1) def count_prefix(self, p): # applied P2: O(m) node = self.root for c in p: node = node.links[self._index(c)] if node is None: return 0 return node.data def sorted_words(self): # in-order DFS: O(T) out = [] self._collect(self.root, [], out) return out def _collect(self, node, path, out): for i, child in enumerate(node.links): # '$' first => "app" before "apple" if child is None: continue if i == 0: out.append(''.join(path)) else: path.append(chr(i - 1 + 97)) self._collect(child, path, out) path.pop()
Expected output: after inserting taco, taro, tarot, coco, chobo, app, apple β search("tar") is False, sorted_words() is ['app', 'apple', 'chobo', 'coco', 'taco', 'taro', 'tarot']; with insert_count over baby, bank, bit β count_prefix("b") == 3, count_prefix("ba") == 2.
π‘ Common Mistake:Building the path with prefix + c strings β each concatenation copies O(M) characters, turning an O(T) traversal into O(TM) β the lectureβs βneedless inefficiencyβ. Append/pop one shared list.
βοΈ Complexity
Operation
Time
Aux space
Note
Insert one word
O(M)
O(1) iterative Β· O(M) recursive
β€M+1 new nodes βΉ O(M)space added
Search
best O(1) Β· worst O(M)
O(1) iterative
M=query length, N never appears
Build N words
O(T)=O(NM)
O(T) nodes
each node holds a k-slot array
Sorted listing
O(T)
O(M) stack + output
in-order, $ first
Prefix count (P2)
O(m)
O(1)
counter maintained during insert
Predecessor (P5)
O(m+n)
O(1)
n= length of the answer
βοΈ Core Decision Matrix
N strings, length M; one string comparison costs O(M).
collisions βΉ not deterministic; can be faster in practice
Trie
O(T)=O(MN)
O(M)deterministic
yes β prefix = a node, in-order = sorted
wasted None slots in every node
When It Flips: need only exact membership and memory is tight β Hash Table. The query mentions a prefix, an order (sorted, predecessor, lexicographic k-th), or demands a worst-case bound β trie.
π Exam Execution Trace & Applied Exercises
Manual Execution Trace β P5 predecessor of candelabra
Trie of canada, canal, candid, candy, cart. ancestor= deepest node with a child < the character being followed.
Step
Char
Nodeβs children
Child < char?
ancestor / ancestor_child
answer
1
c
root: c
no
β
c
2
a
c: a
no
β
ca
3
n
ca: n,r
no
β
can
4
d
can: a,d
a<d
can / a
cand
5
e
cand: i,y
no
can / a
no e child βΉ break
6
back up
pop to can
β
β
canβcana
7
greatest path
cana: d,l βΉ l, then $
β
β
canal
Final Extracted Output:canal. cand is not stored as an ancestor because both its children exceed e. Query ba on {aaa, aba, baa} returns aba; query baa returns itself.
β οΈ Common Mistakes
π‘ Forgetting the $ edge in the height β height is M+1, not M.
π‘ βLeaves =Nβ β leaves β€N; duplicates share one $ leaf β exactly the fact P1 exploits.
π‘ Quoting search as O(logN) or O(MlogN) β a trie never compares whole strings; its cost depends on the query length only.
π‘ Counting a whole subtree per prefix query β O(T); push the count into the node at insert time (P2).
π§ Active Recall
Why does a trie need $, and why put it at links[0]?
Answer
Short answer:$ marks word end so a stored prefix is not mistaken for a word; slot 0 makes the in-order traversal emit words in sorted order.
Why:Membership β after inserting only apple, the walk a-p-p succeeds, so app would be βfoundβ; requiring the $ step separates the word app$ from the prefix app. Ordering β $ precedes every letter, so app is emitted before apple, matching sorted(["apple","app"]).
A sorted array also supports prefix queries. Why is a trie's search O(M) but binary search O(MlogN)?
Answer
Short answer: binary search makes logN whole-string comparisons, each O(M); a trie reads each query character once, with an O(1) array jump per character.
Why:Comparison model β every probe restarts from character 0 of the key. Trie β the path to depth dis the shared prefix, so nothing is re-read and N vanishes from the bound. Price β O(T) nodes each holding k+1 slots β Β§3.
P2 asks for "number of words with prefix p" in O(m) without changing build cost. What goes in the payload, and when is it updated?
Answer
Short answer: an integer counter, incremented on every node visited or created during insert; the query returns the counter of the node reached by p.
Why:Invariant β node.data= number of inserted words whose path passes through node= words with that nodeβs prefix. Cost β one extra O(1) update per insert step βΉ build stays O(T), space stays O(T). The rejected design β counting $ leaves under the node is O(subtree), up to O(T).