Loading content...
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.
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:
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:
Let's see this concretely.
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:
Result: Path root → c → a → t* where * indicates end-of-word
Nodes created: 4 (root + 3 new nodes) Edges created: 3
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 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.
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:
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:
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.
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.
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:
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
An important nuance: a word can be a prefix of another word, and tries handle this naturally.
Consider "car" and "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:
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")
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.
Here's a property that falls naturally out of the sharing structure: in-order traversal of a trie yields strings in lexicographic (alphabetical) order.
Why? Because the trie's structure mirrors the lexicographic ordering! At each node, if you visit children in alphabetical order (a before b before c...), you'll process shorter prefixes before their extensions, and you'll process 'aardvark' before 'apple' before 'zebra'.
The traversal algorithm:
function collectAllWords(node, prefix, results):
if node is end-of-word:
results.add(prefix)
for each child c in alphabetical order:
collectAllWords(c, prefix + c.character, results)
For our example trie:
Visiting children alphabetically from root:
Result: [car, card, care, careful, cat, do, dog] — lexicographically sorted!
Unlike hash tables (which have no inherent order) or BSTs (which require explicit balancing for sorted output), tries give you sorted iteration as a natural consequence of their structure. This is invaluable for autocomplete—suggestions come out alphabetically without additional sorting.
The principle of shared prefixes leading to shared paths is the heart of what makes tries powerful. Let's consolidate:
What's next:
You now understand how multiple strings overlay onto shared paths in a trie. The next page explores character-by-character navigation—the process of traversing a trie one character at a time, which is the foundation for insert, search, and prefix operations.
You've mastered the sharing principle: common prefixes create shared paths. This single insight explains trie space efficiency, prefix query speed, and natural sorted order. Next, we'll see exactly how to navigate these shared paths character by character.