What Is a Trie? Structure & Intuition
Trie as a Prefix Tree
Read aloud is unavailable in this browser.
The Name Says It All
The word trie comes from "retrieval"—and that single etymological fact captures the essence of this data structure. A trie is designed from the ground up for one purpose: retrieving strings efficiently.
But here's what makes tries special: they don't just retrieve strings. They retrieve them by their prefixes. This seemingly simple twist unlocks capabilities that other data structures simply cannot match. When you type "prog" into a search bar and instantly see "programming," "progress," and "program," there's a very good chance a trie (or its variant) is doing the heavy lifting behind the scenes.
By the end of this page, you will understand why a trie is called a 'prefix tree,' how it fundamentally differs from other tree structures you've encountered, and why that difference matters for string-intensive applications. You'll develop an intuition for how tries organize data that will make all subsequent operations seem natural and inevitable.
The Core Identity: A Tree of Prefixes
A trie is a tree-based data structure, but it's fundamentally different from the binary search trees (BSTs) you've studied. The key insight is this:
In a BST, each node holds a complete key. In a trie, the key is distributed across a path from root to node.
This distinction is profound. When you store the word "tree" in a BST, the node contains the entire string "tree." When you store "tree" in a trie, there is no single node that holds "tree"—instead, there's a path through nodes labeled 't', 'r', 'e', 'e' that collectively represents the word.
Let's formalize this:
A trie (also called a prefix tree or digital tree) is a k-ary search tree where:
- Each node represents a single character (or unit) of a key
- The root represents the empty string
- Each path from root to a node represents a prefix
- Complete keys are marked at specific nodes (end-of-word flag)
- Children of a node share a common prefix—the path to that node
Why "prefix tree" is the perfect name:
Consider storing these words: "car," "card," "care," "careful," "cars," "cat."
In a trie, the path from root spelling "c-a-r" is shared by ALL words starting with "car." That path is the prefix "car." The tree literally organizes itself around prefixes—longer words extend shorter prefixes, and all words with the same prefix share the same path up to that prefix.
This is not a design choice. It's the defining characteristic. A trie is a physical manifestation of prefix relationships in the data.
| Aspect | Binary Search Tree | Trie |
|---|---|---|
| Key storage | Complete key in each node | Key distributed across path |
| Node meaning | Node = one key-value pair | Node = one character/unit |
| Search process | Compare entire key at each node | Match one character per node |
| Branching factor | 2 (binary) | Alphabet size (e.g., 26 for lowercase letters) |
| Key representation | Explicit in node | Implicit in path from root |
| Prefix queries | Requires traversal + comparison | Natural—just follow the path |
The Root: Representing Nothing and Everything
In a trie, the root node is special: it represents the empty string (ε). This might seem like a trivial detail, but it's actually fundamental to understanding how tries work.
Think about it: every string has the empty string as a prefix. The string "cat" starts with "", then "c", then "ca", then "cat". By making the root represent the empty string, we establish that every string's path starts at the root—which is exactly what we want for a prefix tree.
This design choice has practical implications:
- •Uniform insertion: Every word, regardless of its first character, is inserted as a child path starting from root. No special cases.
- •Prefix search uniformity: Finding all words with prefix 'X' is identical to the algorithm for finding all words (prefix = empty string)—just start from a different node.
- •Empty string as valid key: If needed, you can store the empty string itself by marking the root as an end-of-word node.
- •Clean recursion: Recursive algorithms don't need base-case checks for the first character—root naturally handles it.
Here's a powerful mental model: the trie rooted at any node N represents all strings that share the prefix spelled by the path from root to N. The entire trie (rooted at root) represents all strings that share the prefix '' (empty string)—which is all strings. Subtries are just 'filtered' views.
Why Not Just Use a Hash Table?
If you've studied hash tables, you might wonder: "Can't I just store strings in a hash set and be done with it?"
For simple membership queries ("Is 'programming' in the dictionary?"), yes—hash tables work beautifully. They give O(1) average-case lookup for exact matches. But the moment you need prefix-aware operations, hash tables fall apart:
- •Exact match lookup: O(1) average
- •Insert: O(1) average
- •Delete: O(1) average
- •Checking membership of complete keys
- •Counting occurrences of exact strings
- •Find all strings with a given prefix
- •Find the longest prefix that exists
- •Return strings in sorted order
- •Find nearest neighbors (edit distance)
- •Support autocomplete efficiently
The fundamental problem with hashing for prefixes:
Hash functions are designed to spread similar inputs across the hash table. The words "program" and "programming" hash to completely different buckets—they're as unrelated to the hash table as "program" and "elephant."
But these words aren't unrelated. "Programming" is an extension of "program." A data structure for string operations should know this. Tries do. Hash tables don't.
Consider autocomplete: given the prefix "pro", find all matching words. With a hash table containing n strings:
| Data Structure | Algorithm | Time Complexity | For 1 million strings, prefix 'pro' |
|---|---|---|---|
| Hash Table | Iterate all strings, check each for prefix | O(n × p) where p = prefix length | ~1,000,000 comparisons |
| Trie | Navigate to 'p'-'r'-'o' node, collect all descendants | O(p + k) where k = matching strings | 3 steps + only matching results |
Hash tables and tries are not competitors—they solve different problems. Use hash tables for exact-match lookups. Use tries when prefixes matter. The choice should be obvious once you understand what each structure is designed for.
The Branching Factor: Alphabet Size Matters
Here's a concept that distinguishes tries from binary trees: the branching factor.
In a binary tree, each node has at most 2 children. In a trie storing lowercase English words, each node can have up to 26 children—one for each letter. This high branching factor is both a superpower and a challenge.
- •Shallow trees: A trie with 26-way branching is much shallower than a binary trie for the same data. Height ≈ length of longest string, not log₂(n).
- •Constant-time child access: With array-based children, jumping to the next character is O(1)—just index into the array.
- •Natural organization: Each 'slot' in a node maps directly to a character. The structure mirrors the data's inherent organization.
- •Memory overhead: Each node needs storage for up to 26 child pointers, even if most are unused (sparse nodes).
- •Alphabet dependency: Unicode tries face 65,536+ possible children per node—impractical with arrays.
- •Trade-off territory: The choice between arrays (fast, memory-heavy) and maps (slower, memory-efficient) becomes critical.
Different alphabets, different tries:
The alphabet size profoundly affects trie design:
- Binary data (bits): 2 children per node → very deep but memory-light
- DNA sequences (A, C, G, T): 4 children → balanced for genomic applications
- Lowercase letters: 26 children → standard English dictionary use case
- Case-sensitive alphanumeric: 62 children → URLs, identifiers
- Full ASCII: 128 children → general text
- Unicode: 65,536+ → requires hash-map based children
This isn't just trivia—it determines whether you use a fixed-size array or a hash map for children, which affects every operation's performance.
For typical English word applications, nodes use fixed-size arrays of 26 pointers. This wastes space for sparse nodes but provides O(1) child access—a trade-off usually worth making. We'll explore alternatives in the node representation module.
A Deeper View: Tries as Finite Automata
For those with a computer science theory background, here's an elegant way to understand tries: a trie is a minimal deterministic finite automaton (DFA) for recognizing a set of strings.
If that sentence made sense to you, the connection is powerful:
- States = nodes
- Transitions = edges labeled with characters
- Start state = root
- Accept states = nodes marked as end-of-word
If automata theory is new to you, don't worry—here's the intuition:
The recognition process:
Imagine you're a robot reading a string character by character: "c", "a", "r", "e".
- You start at the root (state: "haven't read anything yet")
- You see 'c' → follow edge labeled 'c' to a new state (state: "read 'c'")
- You see 'a' → follow edge labeled 'a' (state: "read 'ca'")
- You see 'r' → follow edge labeled 'r' (state: "read 'car'")
- You see 'e' → follow edge labeled 'e' (state: "read 'care'")
- String ends → check if current state is an accept state (end-of-word)
This is exactly how trie search works! The trie is a machine for recognizing whether strings belong to your stored set. Each path from root is a sequence of state transitions that recognizes a specific prefix.
The automaton view explains why tries are so efficient for string matching: there's no backtracking. Each character deterministically leads to exactly one next state. You process each character exactly once. This is fundamentally different from algorithms that might need to reconsider earlier decisions.
The Height Advantage: Depth = String Length
Here's a remarkable property of tries: the height of a trie is determined by the longest string stored, not the number of strings.
In a balanced BST with n strings, height is O(log n). Store a million strings, and you're looking at ~20 levels of tree.
In a trie storing the same million strings, if the longest word is 15 characters, the trie's height is 15. Period. Whether you store 100 strings or 100 million, if the longest is 15 characters, maximum depth is 15.
| Strings Stored | Longest String | Trie Height | Balanced BST Height |
|---|---|---|---|
| 1,000 | 10 chars | 10 | ~10 |
| 10,000 | 10 chars | 10 | ~13 |
| 100,000 | 10 chars | 10 | ~17 |
| 1,000,000 | 10 chars | 10 | ~20 |
| 100,000,000 | 10 chars | 10 | ~27 |
What this means for performance:
Lookup time in a trie is O(m), where m is the length of the search string—completely independent of how many strings are stored. This is powerful:
- Dictionary with 100,000 words: search for "algorithm" = 9 character comparisons
- Dictionary with 1,000,000,000 words: search for "algorithm" = still 9 character comparisons
The BST, meanwhile, requires O(log n) comparisons, and each comparison is itself O(m) for string comparison—giving O(m log n) total. The trie's O(m) wins as n grows, and wins decisively.
A trie's lookup time depends only on the query string length, not on the dataset size. This makes tries ideal for scenarios with massive datasets and bounded-length strings—like word dictionaries, IP addresses, or short codes.
Space Considerations: The Memory Trade-off
Tries are not memory-free. In fact, a naive trie implementation can be a memory hog. Understanding the space trade-offs is essential for making informed decisions about when to use tries.
The optimistic case: lots of shared prefixes
If your strings share many prefixes, tries are space-efficient. Consider storing all English words starting with "pre-": "precise," "predict," "premium," "prepare," etc. The prefix "p-r-e" is stored once, shared by all these words. The more sharing, the less memory.
The pessimistic case: no shared prefixes
If your strings are random with no shared prefixes, a trie degenerates into storing each string along a separate path. Worse, each node might allocate space for 26 child pointers while only using 1. Memory explodes.
- •Prefix overlap: High overlap → shared nodes → less memory. Low overlap → unique paths → more memory.
- •Alphabet size: Larger alphabet → more child pointers per node → more memory (unless using maps).
- •Node representation: Array children (fast, heavy) vs. map children (slower, lighter).
- •String count and length: More/longer strings = more nodes, though overlap mitigates this.
- •End-of-word markers: Small per-node overhead, but adds up across millions of nodes.
Rough memory estimation:
For a trie with n nodes, where each node uses an array of 26 pointers (8 bytes each on 64-bit) plus a boolean end-of-word flag:
- Per node: 26 × 8 + 1 = ~209 bytes
- Total: ~209n bytes
For 1 million nodes: ~209 MB. That's significant!
With hash-map children, you only store actual edges. If average out-degree is 3:
- Per node: ~3 × (1 + 8) + 1 = ~28 bytes (char key + pointer + flag)
- For 1 million nodes: ~28 MB
A 7× memory reduction—but with slower child access (O(1) array vs O(1) amortized map, but with higher constants).
Don't default to tries without considering memory. For small datasets or memory-constrained environments, a sorted array with binary search might be simpler and more efficient. Tries shine for large, prefix-heavy datasets where the query pattern justifies the memory cost.
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.