A trie (from retrieval) is a tree-shaped data structure where each node represents a single character of a key. Keys sharing a common prefix share the same path from the root.
First described by Fredkin (1960) and de la Briandais (1959). Pronounced "try" to distinguish from "tree".
(root)
/ | \
c d t
| | / \
a o h o
/ \ | | |
r t g e p
|
s
(cars) (cat) (dog) (the) (top)
Key insight: "car" and "cars" share the path c → a → r. Shared prefixes mean shared storage — this is the trie's fundamental advantage.
class TrieNode:
def __init__(self):
self.children = [None] * 26 # a-z
self.is_end = False
self.value = None # optional
class TrieNode:
def __init__(self):
self.children = {} # char -> TrieNode
self.is_end = False
| Approach | Child lookup | Memory / node | Best for |
|---|---|---|---|
| Array | O(1) | O(|Σ|) | Small, fixed alphabets |
| Hash map | O(1) amortised | O(children) | Unicode, sparse |
| Sorted list | O(log k) | O(children) | Memory-critical |
Key trade-off: alphabet size |Σ| dominates memory. A 26-slot array wastes space in sparse nodes; a hash map adds overhead per entry.
c in the key: if children[c] exists, move to that child; otherwise create a new node and move to itis_end_of_word = Truedef insert(root, word):
node = root
for char in word:
idx = ord(char) - ord('a')
if node.children[idx] is None:
node.children[idx] = TrieNode()
node = node.children[idx]
node.is_end = True
Complexity: O(m) where m is key length. No rebalancing, no hashing, no comparisons beyond single characters.
After "cat": After "car":
(root) (root)
| |
c c
| |
a a
| / \
t* t* r*
After "card":
(root)
|
c
|
a
/ \
t* r*
|
d*
c: if children[c] is None, return False; else move to that childTrue only if is_end_of_word = Truedef search(root, word):
node = root
for char in word:
idx = ord(char) - ord('a')
if node.children[idx] is None:
return False
node = node.children[idx]
return node.is_end
def starts_with(root, prefix):
node = root
for char in prefix:
idx = ord(char) - ord('a')
if node.children[idx] is None:
return False
node = node.children[idx]
return True # path exists = prefix exists
Key distinction: search("car") requires is_end = True. starts_with("car") only requires the path to exist. This difference is the basis for autocomplete.
is_end_of_worddef delete(root, word, depth=0):
if root is None:
return False
if depth == len(word):
if not root.is_end:
return False
root.is_end = False
return not any(root.children)
idx = ord(word[depth]) - ord('a')
should_delete = delete(
root.children[idx], word, depth + 1)
if should_delete:
root.children[idx] = None
return (not root.is_end
and not any(root.children))
return False
Before: After:
c c
| |
a a
/ \ / \
t* r* t* r*
|
s* <-- removed
Complexity: O(m). Recursive cleanup ensures no orphaned chains waste memory. Nodes shared with other keys ("car") are preserved.
is_end nodesdef autocomplete(root, prefix):
node = root
for char in prefix:
idx = ord(char) - ord('a')
if node.children[idx] is None:
return []
node = node.children[idx]
results = []
_dfs(node, prefix, results)
return results
def _dfs(node, current, results):
if node.is_end:
results.append(current)
for i in range(26):
if node.children[i]:
_dfs(node.children[i],
current + chr(i + ord('a')),
results)
Prefix "ca" -> navigate to 'a' under 'c'
DFS finds: "car", "card", "cars", "cat"
Complexity: O(p + k) where p is prefix length and k is total characters in all matches. No other data structure supports this as efficiently.
Store frequency weights at each end node. Use a priority queue during DFS to return top-k results by popularity.
| Operation | Trie | Hash Table | Balanced BST |
|---|---|---|---|
| Insert | O(m) | O(m) avg | O(m log n) |
| Search | O(m) | O(m) avg | O(m log n) |
| Delete | O(m) | O(m) avg | O(m log n) |
| Prefix search | O(p + k) | O(n · m) | O(n · m) |
| Sorted traversal | O(N) | O(n log n) | O(N) |
m = key length, n = number of keys, p = prefix length, k = output size, N = total characters
O(n · m · |Σ|) — every key branches at every character. Array nodes allocate |Σ| pointers even when mostly NULL.
Heavily shared prefixes reduce space significantly. Hash-map nodes allocate proportional to actual branching only.
Key insight: tries trade space for deterministic O(m) operations. The guarantee is per-key, independent of stored key count — unlike hash tables where collisions degrade performance.
Chains of single-child nodes waste memory:
Standard trie for
{"romane", "romanus", "romulus"}:
r -> o -> m -> a -> n -> e*
-> u -> s*
-> u -> l -> u -> s*
Many single-child nodes.
Merge single-child chains into edges with multi-character labels:
Radix tree:
"rom"
/ \
"an" "ulus"*
/ \
"e"* "us"*
Practical Algorithm To Retrieve Information Coded In Alphanumeric (Morrison, 1968). A radix tree where every internal node has at least two children.
| Property | Standard | Radix/Patricia |
|---|---|---|
Nodes for n keys | O(n · m) | O(n) |
| Operations | O(m) | O(m) |
| Edge labels | Single char | Substrings |
In practice: radix trees are the standard choice for memory-efficient prefix matching. Used in Linux kernel routing tables, Redis cluster key distribution, and HTTP routers.
Each node has three children:
Insert "cat", "cup", "bat":
c
/|\
b a u
| | |
a t* p*
|
t*
| Feature | Standard Trie | TST |
|---|---|---|
| Children / node | |Σ| array | 3 pointers |
| Memory | High (sparse) | Low |
| Search time | O(m) | O(m + log n) |
| Prefix search | Native | Supported |
| Sorted order | In-order | In-order |
Best for: TSTs combine the time efficiency of tries with the space efficiency of BSTs. Particularly effective for large alphabets (Unicode) where array-based tries waste enormous memory.
A suffix trie for string S of length n is a trie containing all n suffixes of S.
Suffixes: banana$, anana$, nana$, ana$, na$, a$, $
Insert all suffixes into a standard trie:
(root)
/ | | | \ \ \
b a n $ ...
| | |
a n a
| | |
n a n
... ... ...
S is a prefix of some suffix — so substrings can be found by prefix searchO(m) — search the trie for the patternO(n2) — each suffix has O(n) charactersSentinel character: the $ ensures no suffix is a prefix of another, guaranteeing every suffix ends at a leaf. The quadratic space cost motivates suffix trees.
Apply path compression (radix tree) to the suffix trie — merge single-child chains into edges labelled with substrings.
Suffix tree for "banana$":
(root)
/ | \
$ a banana$
/ \
$ na
/ \
$ na$
O(n) nodes and edgesO(n) onlineO(m) for pattern of length m(active_node, active_edge, active_length)Power: suffix trees solve dozens of string problems in optimal time — pattern matching, longest repeated substring, string statistics, and more.
A sorted array of all suffix starting positions. Uses less memory than suffix trees with similar query capabilities. Constructible in O(n) via SA-IS algorithm.
A generalised suffix tree (GST) stores the suffixes of multiple strings simultaneously. Each leaf is labelled with both a string index and suffix position.
banana$1bandana$2Construction remains O(N) where N is the total length of all strings.
| Problem | Solution via GST |
|---|---|
| Longest common substring | Deepest internal node with leaves from all strings |
| Common substrings of k/n | Nodes with leaves from ≥ k distinct strings |
| Suffix-prefix overlap | Used in genome assembly overlap graphs |
| Multiple pattern search | Build GST of patterns; stream text through it |
Bioinformatics: GSTs are foundational in genomics. Assemblers like Velvet and SGA use them to find overlapping reads efficiently.
| Feature | Trie | Hash Table | Balanced BST |
|---|---|---|---|
| Search | O(m) deterministic | O(m) avg, O(nm) worst | O(m log n) |
| Insert | O(m) deterministic | O(m) avg, O(nm) worst | O(m log n) |
| Prefix queries | Native, O(p + k) | Not supported | Not supported |
| Sorted order | Natural (DFS) | Requires sorting | Natural (in-order) |
| Worst-case guarantee | Always O(m) | Degrades with collisions | O(m log n) |
| Memory | High (pointer-heavy) | Moderate | Moderate |
| Cache locality | Poor (pointer chasing) | Good (contiguous) | Moderate |
Prefix queries, autocomplete, longest prefix match, sorted iteration needed
Exact-match lookups only, no prefix queries, memory-sensitive workloads
Sorted order and range queries needed, no prefix operations required
Replace fixed-size child arrays with a bitmask + compact array. Only non-null pointers are stored. Per-node memory drops from O(|Σ|) to O(children).
Map characters to smaller ranges. Case-insensitive a-z = 26 slots instead of 256 for ASCII. Encode Unicode as UTF-8 bytes — alphabet drops to 256.
Eliminate single-child chains. Node count drops from O(n · m) to O(n).
Level-Order Unary Degree Sequence: encode trie topology in ~2 bits per node. Store labels in a flat array. Near-theoretical-minimum space with rank/select for navigation.
Two parallel arrays: base[] and check[]. Achieves array-trie speed with compressed memory. Used in Japanese morphological analysers (MeCab, Darts).
In practice: a radix tree with hash-map nodes covers most use cases. Succinct tries (LOUDS, DFUDS) are reserved for static dictionaries where minimising memory is paramount.
A trie over the alphabet {0, 1}. Keys are bit strings. Each level inspects one bit.
Insert 5 (101), 2 (010), 7 (111):
(root)
0/ \1
(0) (1)
0/ \1 0/ \1
( ) ( ) (1) ( )
| | | |
... 2* 5* 7*
Only branch at "critical bits" where keys differ — skip identical prefix bits. Minimal branching, fast lookups.
| Application | Why bitwise trie? |
|---|---|
| IP routing (LPM) | Match destination IP against prefixes bit by bit |
| XOR nearest neighbour | Kademlia DHT uses XOR distance on bitwise tries |
| Integer sets | Predecessor/successor in O(w) for w-bit keys |
| Van Emde Boas | Bitwise trie over integer universe, O(log log U) |
Linux kernel: IPv4/IPv6 longest prefix match uses a multi-bit trie (LC-trie) — inspecting several bits per level for speed.
Standard tries have poor cache locality — every character lookup follows a pointer to a different memory location.
Burst trie:
(root)
/ \
a b
| |
[pple, [at, oy,
rt, us]*
xe]*
* = container (sorted list)
| Feature | Standard Trie | Burst Trie |
|---|---|---|
| Cache misses | One per character | Amortised — fewer dereferences |
| Memory | High | Lower (compact containers) |
| Insert | O(m) | O(m) amortised |
| Search | O(m) | O(m) amortised |
Origin: Heinz, Zobel & Williams (2002). Burst tries consistently outperform standard tries and hash tables on string-heavy workloads due to superior cache behaviour.
str. — list methods starting with that prefixWeighted trie for autocomplete:
(root)
|
c
|
a
/ \
r t (freq: 500)
|
(freq: 300)
|
s (freq: 100)
d: traverse the trie tracking insertions, deletions, substitutions (Levenshtein)dBoggle / Scrabble solvers: DFS on the board guided by trie edges — prune invalid prefixes immediately.
Scale: Google's autocomplete processes billions of queries daily using distributed trie-like structures with frequency-weighted ranking.
Routers store rules as IP prefixes with varying lengths. For each packet, find the longest matching prefix.
Routing table:
10.0.0.0/8 -> Gateway A
10.1.0.0/16 -> Gateway B
10.1.1.0/24 -> Gateway C
Packet to 10.1.1.42:
Matches all three
Longest prefix = /24 -> Gateway C
O(W) lookup (W = 32 for IPv4, 128 for IPv6)Map phone keypad digits to letters: 2=abc, 3=def, ... Build a trie keyed on digit sequences.
Input: 4-3-5-5-6
Trie path: 4 -> 3 -> 5 -> 5 -> 6
Matches: "hello", "jello"
Ubiquity: tries are embedded in systems developers use daily — from the router forwarding their packets to the keyboard predicting their words.
O(m) — independent of stored key countO(n) nodes| Source | Description |
|---|---|
| Sedgewick & Wayne | Algorithms, 4th Ed. — tries and TSTs |
| Gusfield | Algorithms on Strings, Trees, and Sequences — suffix trees |
| Ukkonen (1995) | On-line construction of suffix trees |
| Heinz et al. (2002) | Burst Tries — fast string key structure |
| Knuth | TAOCP Vol. 3 — digital searching |
| Morrison (1968) | PATRICIA — the original compressed trie |