What Is a Trie? Structure & Intuition
Shared Prefixes, Shared Paths
Read aloud is unavailable in this browser.
The Power of Overlap
Consider the English dictionary. It contains thousands of words starting with "pre-": precise, predict, premium, prepare, present, preserve, prevent. Each word is unique, yet they all share a common beginning.
A naive data structure stores each word independently, repeating "p-r-e" thousands of times. A trie does something elegant: it stores the prefix once.
This single principle—shared prefixes lead to shared paths—is the engine that drives everything a trie can do. It's why tries are space-efficient for overlapping data, why prefix queries are fast, and why the structure naturally organizes strings by their common beginnings.
By the end of this page, you will understand exactly how multiple strings merge onto shared paths in a trie, how this sharing affects storage and retrieval, and how to visualize the overlay process. You'll see that this isn't just an optimization—it's the defining characteristic that makes tries unique.
The Fundamental Principle
Let's state the principle precisely and then explore its implications:
If two strings share a prefix of length k, their paths through the trie share exactly the first k edges and k+1 nodes (including the root).
This is not a design choice—it's a mathematical consequence of how tries are constructed. When you insert a string into a trie, you:
- Start at the root
- For each character, follow the corresponding edge if it exists, or create it if it doesn't
- After the last character, mark the node as an end-of-word
The key insight is step 2: you follow existing edges when they exist. Two strings that start with the same characters will follow the same edges for those characters.
Consider strings "car" and "card":
• "car" creates path: root → c → a → r (mark end-of-word) • "card" follows: root → c → a → r (already exist!) → d (create, mark end-of-word)
The path c→a→r is shared. The 'd' extends it. "car" is both a complete word AND a prefix of "card"—the trie naturally represents both relationships.
Why this matters:
This sharing has profound implications:
- Space efficiency: No redundant storage of common prefixes
- Query efficiency: Navigating to any prefix automatically positions you to find all words sharing that prefix
- Natural grouping: Words with common prefixes cluster together in the structure
- Implicit hierarchy: Shorter words are prefixes of longer related words
Let's see this concretely.
Building a Trie: Step by Step
Let's build a trie inserting words one at a time and observe how paths merge. We'll insert the words: cat, car, card, care, careful, dog, do.
Pay close attention to which nodes and edges are reused.
Inserting 'cat' into empty trie:
- Start at root (empty trie)
- Character 'c': no 'c' edge from root → create node, add edge labeled 'c'
- Character 'a': no 'a' edge from current node → create node, add edge labeled 'a'
- Character 't': no 't' edge → create node, add edge labeled 't'
- End of word: mark current node as end-of-word
Result: Path root → c → a → t* where * indicates end-of-word
Nodes created: 4 (root + 3 new nodes) Edges created: 3
Visualizing the Merge
Let's visualize the final trie structure. I'll use ASCII art to show how paths merge and diverge:
[root]
/
c d
| |
a o* ← 'do' ends here
/ \\ |
t* r* g* ← 'cat', 'car', 'dog' end here
/
d* e* ← 'card', 'care' end here
|
f
|
u
|
l* ← 'careful' ends here
Key structural observations:
- •The root branches into 'c' and 'd': These are the two distinct first-character families in our word set.
- •The 'c' branch merges at 'a': Both 'cat' and the 'car*' family share c→a.
- •The 'a' node has two children ('t' and 'r'): This is where 'cat' diverges from the 'car*' family.
- •The 'r' node (for 'car') has two children ('d' and 'e'): Further branching for 'card' and 'care/careful'.
- •Nodes marked with * are end-of-word: They can simultaneously be intermediate nodes for longer words.
The compression is dramatic:
| Metric | Without Sharing | With Trie | Savings |
|---|---|---|---|
| Characters stored | 26 | 12 edges | 54% |
| 'car' prefix repetitions | 4 times | 1 time | 75% |
| 'care' prefix repetitions | 2 times | 1 time | 50% |
The savings compound with more words. A dictionary with 10,000 'pre-' words stores p→r→e exactly once, not 10,000 times.
Why Sharing Enables Fast Prefix Queries
Here's where the magic happens. Because words with a common prefix share a path, navigating to the end of that path positions you at the root of a subtrie containing exactly those words.
Let's trace a prefix query: "Find all words starting with 'car'"
Step 1: Navigate to prefix
root → 'c' → 'a' → 'r'
We're now at the node representing the prefix "car".
Step 2: Collect all words in this subtrie From this node, we can reach:
- This node itself (if end-of-word) → "car" ✓
- Path to 'd' (end-of-word) → "card" ✓
- Path to 'e' (end-of-word) → "care" ✓
- Path to 'e' → 'f' → 'u' → 'l' (end-of-word) → "careful" ✓
Result: {car, card, care, careful} — exactly the words with prefix "car"!
Every node in a trie is the root of a subtrie containing all words that share the prefix represented by the path to that node. Prefix queries are just 'navigate to prefix node + enumerate subtrie'. The sharing property GUARANTEES that all matching words (and only matching words) are in that subtrie.
Complexity analysis:
For prefix query with prefix P of length p, returning k matching words:
- Navigate to prefix node: O(p) — follow p edges
- Enumerate subtrie: O(total characters in results) — proportional to output size
Total: O(p + output size)
Compare this to scanning all n strings:
| Approach | Time Complexity | For 1M strings, prefix 'car' |
|---|---|---|
| Linear scan | O(n × p) | ~3 million char comparisons |
| Trie | O(p + k) | 3 steps + result traversal |
The trie's advantage grows with dataset size because navigation time is independent of n.
The Overlapping Paths Principle
Let's formalize the sharing mathematically. This will help you predict trie behavior and analyze space complexity.
Definition: Longest Common Prefix (LCP)
For strings s₁ and s₂, the longest common prefix LCP(s₁, s₂) is the longest string that is a prefix of both.
- LCP("car", "card") = "car"
- LCP("cat", "car") = "ca"
- LCP("dog", "car") = "" (empty)
The Sharing Theorem:
Strings s₁ and s₂ share exactly |LCP(s₁, s₂)| + 1 nodes in the trie, where the +1 is for the root.
Extending to multiple strings:
For a set of strings S = {s₁, s₂, ..., sₙ}, the node count of their trie is:
nodes = 1 + (total edges) = 1 + (sum of unique character positions across all strings)
More intuitively: every unique (prefix, next-character) pair creates one edge.
If "car" and "card" are in S:
- "car" contributes pairs: ("", c), (c, a), (ca, r) → 3 edges
- "card" contributes: ("", c), (c, a), (ca, r), (car, d) → but first 3 already exist! Only 1 new edge
Total edges for {car, card}: 4 (not 7) Total nodes: 5 (edges + 1 for root)
For a trie containing n strings with total length L and total unique (prefix, char) pairs U:
• Number of nodes = U + 1 • Number of edges = U • U ≤ L (equality when no prefix sharing) • U can be much smaller than L with heavy sharing
Best case: n strings of length m, all starting the same → O(m) nodes Worst case: n strings with no common prefixes → O(L) = O(n × avg_length) nodes
Prefix Containment and Word Boundaries
An important nuance: a word can be a prefix of another word, and tries handle this naturally.
Consider "car" and "card":
- "car" is a complete word
- "car" is also a prefix of "card"
The trie handles this seamlessly because end-of-word is a node property, not a structural property. The node at the end of path c→a→r is marked as end-of-word (for "car") AND it has a child 'd' (continuing to "card").
This is crucial for correctness:
- •Without markers, you can't distinguish words from prefixes: Is "car" stored, or just "card"? Without checking the 'r' node's end-of-word flag, you can't tell.
- •Path existence ≠ word existence: The path c→a→r→e exists (for "care" and "careful"), but that doesn't mean "car", "ca", or "c" are stored words.
- •Search must check the flag: When searching for "car", finding the path isn't enough—you must verify the final node is marked as end-of-word.
The general pattern:
Every string creates a path. Only some nodes on that path are end-of-word nodes. Specifically, a node is end-of-word if and only if a stored string ends at that node.
Stored words: {a, an, ant, antenna}
Path: root → a* → n* → t* → e → n → n → a*
↑ ↑ ↑ ↑
end-of-word markers
• 'a' node: end-of-word (for "a")
• 'n' node: end-of-word (for "an")
• 't' node: end-of-word (for "ant")
• 'e', first 'n', second 'n' nodes: NOT end-of-word
• final 'a' node: end-of-word (for "antenna")
Measuring Prefix Overlap: The Compression Ratio
How much space does a trie actually save? It depends entirely on how much your strings overlap. Let's develop intuition for when tries shine.
Compression ratio:
compression_ratio = (total_characters_in_all_strings) / (number_of_trie_edges)
A ratio of 1 means no compression (no shared prefixes). A ratio of 10 means 10× compression (heavy sharing).
| Data Pattern | Example | Approximate Ratio | Trie Benefit |
|---|---|---|---|
| Random strings | "xkq", "plm", "wvb" | ~1 | None (consider hash table) |
| Common suffix only | "running", "jumping", "sleeping" | ~1 | None (tries share prefixes, not suffixes) |
| Dictionary words | English word list | ~3-5 | Moderate |
| Common prefix family | "program", "programming", "programmer" | ~2-3 | Good |
| URL paths | "/api/v1/users", "/api/v1/posts" | ~5-10 | Excellent |
| IP addresses | "192.168.1.*" range | ~10+ | Excellent |
Data with high prefix overlap includes: • Words with common roots (un-, re-, pre-, anti-) • Hierarchical paths (URLs, file paths) • Structured identifiers (IP addresses, product codes) • Auto-generated sequences (log timestamps)
Data with low overlap: random hashes, UUIDs, encrypted strings, natural language text treated as single strings.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.