Think about the search bar on a phone or website. As soon as you type:
cayou might start seeing suggestions such as:
cat
car
card
care
cameraAll of these words share the same beginning. Instead of treating every word as completely independent, we can organize them around their shared characters.
A Trie is a tree-based data structure built specifically for this kind of problem.
A Trie stores strings character by character and is especially efficient when the problem involves prefixes.
Trie is pronounced “try.”
How a Trie Represents Words
Suppose we insert:
cat
car
cardA simplified Trie could look like:
root
|
c
|
a
/ \
t r
|
dEach path from the root represents characters in a word:
root → c → a → t = "cat"
root → c → a → r = "car"
root → c → a → r → d = "card"Notice that cat, car, and card all share the path:
c → aThe Trie stores that common prefix once instead of creating completely separate structures.
Trie Nodes
A Trie is made up of nodes. Each node needs to know which characters can come next.
For lowercase English letters, there are 26 possible next characters:
a → 0
b → 1
c → 2
...
z → 25A simple Java node can therefore be represented as:
class TrieNode {
TrieNode[] children =
new TrieNode[26];
boolean isEndOfWord;
}The children array points toward the next possible characters.
The isEndOfWord field is equally important because a path can represent a prefix without representing a complete word.
For example, if we store:
applethen:
appis a valid prefix, but it may not be a complete word that we inserted.
So the node for p needs to know whether a word actually ends there.
The Root Node
Every Trie starts with a root.
The root doesn't represent a character. It simply provides the starting point for all stored strings.
For example:
root
|
c
|
a
|
tThe path from the root represents "cat".
Inserting a Word
Let's insert:
catWe begin at the root and process one character at a time.
First comes:
cThen:
aThen:
tFinally, we mark the t node as the end of a complete word.
Conceptually:
root
|
c
|
a
|
t* The * means:
isEndOfWord = trueIn Java, the insertion operation can be written as:
class Trie {
TrieNode root;
Trie() {
root = new TrieNode();
}
void insert(String word) {
TrieNode current = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (current.children[index] == null) {
current.children[index] = new TrieNode();
}
current = current.children[index];
}
current.isEndOfWord = true;
}
}The expression:
int index = ch - 'a';converts a lowercase character into an array index.
For example:
'a' - 'a' = 0
'b' - 'a' = 1
'c' - 'a' = 2So the character c is stored at:
children[2]Sharing Prefixes
Now suppose we insert:
cat
car
cardAfter inserting cat:
root
|
c
|
a
|
t*When inserting car, we don't create another c and a.
Those nodes already exist.
We simply continue from them:
c
|
a
/ \
t* r*When card is inserted, the existing c → a → r path is reused:
c
|
a
/ \
t* r*
|
d*This sharing of common prefixes is one of the defining characteristics of a Trie.
Searching for a Word
Suppose the Trie contains:
cat
car
cardand we want to search for:
carWe follow:
c → a → rIf all three nodes exist, we then check:
current.isEndOfWordIf it is true, "car" was actually stored as a complete word.
A Java implementation is:
boolean search(String word) {
TrieNode current = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (current.children[index] == null) {
return false;
}
current = current.children[index];
}
return current.isEndOfWord;
}That final isEndOfWord check matters.
Suppose only:
applewas inserted.
The path for:
appexists, but that doesn't mean "app" was inserted as a complete word.
So:
Path exists → Prefix exists
isEndOfWord=true → Complete word existsChecking a Prefix
A Trie becomes particularly useful when we don't need to find a complete word.
Suppose we have:
cat
car
card
care
dogand ask:
Does any stored word start with "car"?We only need to follow:
c → a → rIf that path exists, then "car" is a valid prefix.
We don't care whether the final node has isEndOfWord = true.
In Java:
boolean startsWith(String prefix) {
TrieNode current = root;
for (char ch : prefix.toCharArray()) {
int index = ch - 'a';
if (current.children[index] == null) {
return false;
}
current = current.children[index];
}
return true;
}Therefore:
startsWith("car");returns:
truewhile:
startsWith("xyz");returns:
falseA Complete Trie Implementation
Here is a basic Trie supporting insertion, complete-word searching, and prefix searching:
class TrieNode {
TrieNode[] children =
new TrieNode[26];
boolean isEndOfWord;
}
class Trie {
TrieNode root;
Trie() {
root = new TrieNode();
}
void insert(String word) {
TrieNode current = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (current.children[index] == null) {
current.children[index] =
new TrieNode();
}
current = current.children[index];
}
current.isEndOfWord = true;
}
boolean search(String word) {
TrieNode current = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (current.children[index] == null) {
return false;
}
current = current.children[index];
}
return current.isEndOfWord;
}
boolean startsWith(String prefix) {
TrieNode current = root;
for (char ch : prefix.toCharArray()) {
int index = ch - 'a';
if (current.children[index] == null) {
return false;
}
current = current.children[index];
}
return true;
}
}We can use it like this:
class Main {
public static void main(String[] args) {
Trie trie = new Trie();
trie.insert("cat");
trie.insert("car");
trie.insert("card");
System.out.println(
trie.search("car")
);
System.out.println(
trie.search("can")
);
System.out.println(
trie.startsWith("ca")
);
}
}Output:
true
false
trueTime Complexity
If the length of the word or prefix is L, the basic Trie operations take:
Insert → O(L)
Search → O(L)
Prefix Search → O(L)Why?
Because the Trie processes one character at a time.
For example, searching:
catrequires following:
c
a
tSo the number of operations depends primarily on the string's length.
Space Complexity
The major drawback of a Trie is memory consumption.
With:
TrieNode[] children = new TrieNode[26];every node has room for 26 child references.
If many words have different prefixes, the Trie may require a large number of nodes.
On the other hand, shared prefixes can reduce duplication.
So the trade-off is roughly:
Trie
→ Excellent prefix operations
→ Potentially high memory usageCompared with:
HashSet / HashMap
→ Often simpler
→ Usually less memory-intensive
→ Not naturally designed for prefix queriesTrie vs HashSet
Suppose we have:
cat
car
card
care
dogA HashSet<String> is excellent for asking:
Does "car" exist?But if we ask:
What words begin with "car"?a HashSet doesn't naturally represent that prefix relationship.
A Trie does:
root
|
c
|
a
|
r
/ \
d eThe shared path itself represents the prefix.
So the basic distinction is:
HashSet → Exact membership
Trie → Exact membership + Prefix operationsTrie vs HashMap
A HashMap is designed around:
Key → ValueFor example:
"cat" → 10
"car" → 20It can perform exact key lookups efficiently.
A Trie instead organizes strings according to their characters:
First character
↓
Second character
↓
Third character
↓
...So their primary strengths differ:
HashMap → Key-value lookup
Trie → Character and prefix-based lookupAutocomplete
One of the most recognizable uses of a Trie is autocomplete.
Imagine a search box containing a dictionary of:
cat
car
card
care
cameraThe user types:
caThe Trie takes us to:
root → c → aFrom that node, we can explore the remaining branches and find possible completions:
cat
car
card
care
cameraThis makes Tries particularly suitable for autocomplete systems.
Spell Checking
A Trie can also represent a dictionary of valid words:
apple
banana
computer
developerWhen a user enters:
computrthe Trie can help determine whether that exact word exists.
More advanced algorithms can then be combined with the Trie to find similar words for correction.
The important point here is that the Trie gives us an efficient representation of the dictionary.
Longest Common Prefix
Tries are also useful for problems involving common prefixes.
Consider:
flower
flow
flightThe longest common prefix is:
flA Trie naturally represents shared beginnings:
root
|
f
|
l
/ \
o iThe path shared by all words before the structure branches represents their common prefix.
Counting Words Sharing a Prefix
A Trie can be extended to store additional information at each node.
For example:
int prefixCount;Suppose we store:
car
card
careAt the node representing:
carwe could store:
prefixCount = 3That allows us to answer questions such as:
How many stored words start with "car"?with the appropriate additional bookkeeping.
This is a common technique in more advanced Trie problems.
Binary Trie
A Trie doesn't have to work with alphabetic characters.
We can also build a Binary Trie using only:
0
1For example:
101
100
110can be represented using two possible branches at every level.
A binary Trie is especially useful for problems involving:
XOR
Maximum XOR
Bit manipulation
Binary numbers
For example, finding the pair of numbers that produces the maximum XOR is a classic application.
A binary Trie node can be as simple as:
class TrieNode {
TrieNode[] children =
new TrieNode[2];
}Here:
children[0] → bit 0
children[1] → bit 1The underlying idea is exactly the same as a normal Trie.
Trie vs Binary Search Tree
A Binary Search Tree organizes values according to comparisons:
Smaller → Left
Larger → RightA Trie organizes strings one character at a time.
For example:
BST:
50
/ \
30 70while a Trie might look like:
root
|
c
|
a
/ \
t rSo:
BST → Ordered values
Trie → Character/prefix structureThey're both tree-based structures, but they solve different types of problems.
Practical Applications
Tries are useful whenever strings and prefixes are central to the problem.
Common examples include:
Autocomplete
Dictionary lookup
Prefix searching
Spell checking
Longest common prefix
Word-search problems
Prefix-frequency queries
Binary XOR problems
The important question to ask in a DSA problem is:
"Do I need to work with prefixes rather than just exact values?"
If yes, a Trie may be a strong candidate.
The Main Idea
A Trie stores strings as paths through a tree:
root
|
c
|
a
/ \
t r
|
dwhich can represent:
cat
car
cardIts core operations are:
insert() → O(L)
search() → O(L)
startsWith() → O(L)where L is the length of the word or prefix.
The central idea is shared prefixes:
cat
car
cardshare:
c → aand:
car
cardshare:
c → a → rA Trie organizes strings character by character, making it especially powerful for prefix-based operations such as autocomplete, dictionary lookup, and prefix searching.
Keep this distinction in mind:
HashSet → Exact lookup
HashMap → Key-value lookup
BST → Ordered lookup
Trie → Prefix-based string lookupOnce the basic Trie is clear, more advanced problems can build on the same structure, including Trie deletion, prefix counting, autocomplete, word search, and Binary Tries for maximum-XOR problems.