skip to content

An autocomplete trie sized for ASCII blows its memory ceiling on a multilingual catalog — how do you decide what to change?

level: principalimportance: should knowfreq 30%

answer

  1. what does each node reserve for children
  2. fanout is now the whole symbol range
  3. most nodes have one or two children
  4. index encoded bytes, not symbols
  5. or stop storing a trie at all

basics

~20 s

Measure first: node count, fanout and bytes per node. The usual culprit is a child slot reserved per possible symbol — fine for a tiny alphabet, impossible for a universal one. The levers: children representation, compression, byte indexing, or no trie at all.

solid answer

~50 s

Total memory is node count times per-node cost, so find out which term exploded. Widening the symbol range does not add nodes — it multiplies the cost of every node that reserves one slot per possible symbol, and most nodes have one or two children, so nearly all of that is empty. The levers, by effort: swap fixed child arrays for a small sorted list of symbol-child pairs, which suits fanout of one or two; compress non-branching chains so node count falls to roughly the key count; or index the encoded byte stream so fanout is capped at 256 and compact arrays return, accepting deeper paths and a normalization requirement. If the only query is ranked completions, drop the trie for precomputed top-k lists per short prefix, or sorted keys with a binary-searched prefix range. Weigh each against the latency budget and what the team can maintain.

go deeper

for a junior

Know that every trie node must represent its children somehow, and that giving each node one slot per possible symbol is only affordable when the symbol range is small.

for a middle

Explain how node count and per-node fanout storage multiply into total memory, and compare fixed arrays, small sorted pair lists and per-node maps for nodes that typically hold one or two children.

for a senior

Diagnose before redesigning: measure node count, the fanout histogram and bytes per node, then show which of compression, a compact children representation or a byte-level index actually moves the total.

for a principal

Weigh the memory ceiling against the latency budget, the write path and what the team can operate on call. Sequence the migration so correctness changes land before space changes, and justify picking a larger structure that stays debuggable.

**Decompose the number before choosing a fix** Total memory is *node count* x *bytes per node*. A symbol-range change moves only the second factor, and it moves it violently, so the first diagnostic is a histogram, not a redesign: how many nodes, what is each one's fanout, how many bytes does each occupy. In nearly every version of this incident the answer is that most nodes have one or two children while each reserves capacity for the entire symbol range. **Lever 1: how a node represents its children** - **Fixed array over the symbol range.** Child lookup is a single index, which is the fastest per step and the reason the prototype was written this way. Cost scales with the symbol range and is paid by every node whether or not it has children. Fine for a tiny alphabet, ruinous for a universal one. - **Small sorted list of (symbol, child) pairs.** Cost is proportional to actual children. For fanout one to four — the overwhelming majority of nodes — a linear scan of a contiguous few pairs is both smaller and, thanks to locality, often faster than anything cleverer. Binary search inside the node handles the rare wide node. - **Per-node hash map.** Cost is also proportional to actual children, and per-step lookup is a hash plus a probe. The catch is fixed overhead per map instance, which for a node with a single child can exceed the child pointer several times over; use it only for genuinely wide nodes. - **Adaptive nodes.** Pick the representation per node by current fanout, promoting a small list to an indexed array when a node becomes wide. This is what serious radix-trie designs do, and it is also the option with the most code to maintain. **Lever 2: compression, which attacks the other factor** Collapse every maximal non-branching chain into one edge labelled with the whole substring. Node count drops from proportional-to-total-symbols to proportional-to-key-count. On a catalogue where most entries have a long unique tail this is usually the single largest win, and it composes with any children representation. It does not reduce the symbols compared during a lookup, and it complicates insertion, which must now split an edge when a new key diverges mid-label. **Lever 3: index bytes instead of symbols** If keys are stored in a variable-width encoding, build the trie over the encoded bytes. Fanout is then capped at 256 and compact per-node arrays become affordable again. Two consequences must be stated aloud, because this is where correctness bugs enter: 1. Paths get up to four times deeper for non-Latin text, so lookups chase more pointers. 2. A byte prefix is not a user-visible character prefix. A partially consumed multi-byte symbol must never surface as a completion, and prefixes must be **normalized and casefolded before insertion and before query**, or the same visible word will fail to match itself depending on how it was composed. Decide once, at the boundary, and store the folded form. **Lever 4: stop storing a trie** The structure is a means. If the only query is "top-k completions for this prefix": - **Precompute per prefix.** For prefixes up to k symbols, store the ranked completion list directly. Memory is bounded by a number you choose, latency is one lookup, and rebuilds are a batch job. You lose arbitrary-depth prefixes, which for a search box is usually acceptable after a handful of symbols. - **Sorted keys plus binary search.** Keys in sorted order make any prefix a contiguous range, found in O(L log n) comparisons. Memory is near the raw key bytes with no structural overhead, and the layout is contiguous and cache-friendly. - **Deduplicate shared suffixes.** Merging identical suffix subtrees turns the trie into an acyclic word graph, which is dramatically smaller for a word list. The cost is that per-key payloads no longer hang naturally off nodes, and incremental updates become hard — a good fit for a structure rebuilt offline, a bad one for live writes. **The judgment, which is the actual question** Rank the levers against three constraints rather than against smallest-wins: - *The ceiling and the budget.* How much do you need to save, and what per-lookup latency can you spend to save it? Compression plus a compact children representation typically clears an order of magnitude with no exotic structure; reach for a succinct encoding only if that is not enough. - *The write path.* If the catalogue updates continuously, structures that must be rebuilt offline are out regardless of their size, or need a two-structure design with a small live overlay merged periodically. - *Who maintains it.* A succinct structure one engineer understands is a liability at 3am, and its debugging story is much worse than a sorted array's. Choosing the second-smallest option that any team member can reason about is a defensible senior-leadership call, and saying so explicitly is what distinguishes this answer from a list of data structures. Finally, sequence the migration: correctness first (normalization and folding, which change *results*), then the space fixes, and keep the old index queryable behind a flag until the new one's completion sets match on real traffic. **The one-line takeaway** Alphabet size attacks bytes-per-node, so fix the children representation and compress the chains before considering exotic structures — and pick the option the team can operate, not the smallest one on paper.

  • What does switching per-node children from a fixed array to a hash map actually cost?
    Memory becomes proportional to real children rather than to the symbol range, which is the win. The costs are a hash plus a probe on every step instead of one index, worse locality, and a fixed per-map overhead that for a node with one child can exceed what it stores. Since most nodes have one or two children, a small sorted pair list often beats it on both axes.
  • What breaks if you index the encoded byte stream instead of symbols?
    Fanout is capped at 256 so compact arrays are affordable again, and byte prefixes still behave as prefixes. But paths get up to four times deeper for non-Latin text, and a prefix boundary no longer aligns with a visible character — a partially consumed multi-byte symbol must not surface as a match. You also have to normalize and casefold identically at insert and query time.
  • When would you drop the trie entirely?
    When the only query is ranked completions. Precomputing the top-k list for every prefix up to a few symbols bounds memory by a number you pick and answers in one lookup; alternatively, sorted keys turn any prefix into a contiguous range found in O(L log n). You trade generality for a footprint you can budget and a structure anyone on the team can debug.

saying these in an interview costs you the question

  • Keeps a fixed alphabet-sized child array per node
  • Redesigns before measuring node count and fanout
  • Treats a symbol trie and a byte trie as equivalent
  • Skips normalization and casefolding before insertion
  • Proposes a succinct structure nobody else can maintain
  • Assumes memory scales with key count, not node count

context