DEV Community

Shankar L
Shankar L

Posted on

Trie The Data Structure Behind Fast Prefix Searching

Why should you care?

Imagine typing app into a search box and immediately getting:

  • apple
  • application
  • apply
  • appointment

How does the system find all words beginning with app so efficiently?

One data structure designed specifically for this kind of problem is a Trie.

Tries are useful for:

  • Autocomplete
  • Spell checkers
  • Search suggestions
  • Dictionary implementations
  • Prefix matching
  • IP routing
  • Word games
  • Text processing

Unlike many data structures we've studied so far, a Trie is designed around characters and prefixes.


The Problem

Suppose we have these words:

apple
app
application
apply
banana
band
bandage
Enter fullscreen mode Exit fullscreen mode

We want to answer questions such as:

Does "apple" exist?
Does "app" exist?
What words start with "app"?
What words start with "ban"?
Enter fullscreen mode Exit fullscreen mode

A simple approach is to store the words in an array or list and compare each word.

For example:

apple
application
apply
banana
band
bandage
Enter fullscreen mode Exit fullscreen mode

To find words beginning with app, we may have to inspect many strings.

Hash tables are excellent for checking whether an exact word exists:

"apple" → exists
"app"   → exists
Enter fullscreen mode Exit fullscreen mode

But they are not naturally designed for:

"Give me every word beginning with app"
Enter fullscreen mode Exit fullscreen mode

We need a structure that represents shared prefixes efficiently.

That is where a Trie comes in.


The Concept

A Trie is a tree-like data structure used to store strings.

Each path from the root represents characters in a word.

For example, consider:

app
apple
apply
Enter fullscreen mode Exit fullscreen mode

The Trie can look conceptually like:

        root
          |
          a
          |
          p
          |
          p
        / | \
       l  ... y
       |
       e
Enter fullscreen mode Exit fullscreen mode

The important idea is that common prefixes are shared.

The words:

app
apple
application
apply
Enter fullscreen mode Exit fullscreen mode

all begin with:

app
Enter fullscreen mode Exit fullscreen mode

Instead of storing app repeatedly, the Trie stores those characters along one shared path.

Each node generally contains:

  • Links to child nodes
  • Information about whether a complete word ends there

For example:

Node
 ├── children
 └── isEndOfWord
Enter fullscreen mode Exit fullscreen mode

Simple Explanation

Think of a Trie as a character-by-character dictionary.

Suppose we insert:

cat
car
can
Enter fullscreen mode Exit fullscreen mode

The Trie begins with:

root
 |
 c
 |
 a
Enter fullscreen mode Exit fullscreen mode

Then the paths split:

       c
       |
       a
     / | \
    t  r  n
Enter fullscreen mode Exit fullscreen mode

The prefix:

ca
Enter fullscreen mode Exit fullscreen mode

is shared.

But the final characters:

t
r
n
Enter fullscreen mode Exit fullscreen mode

represent different words.

The key point is:

Each level of the Trie represents another character in the word.

So if we search for:

car
Enter fullscreen mode Exit fullscreen mode

we follow:

root → c → a → r
Enter fullscreen mode Exit fullscreen mode

If the r node marks the end of a word, car exists.


Real-world Analogy

Imagine a large dictionary organized like a filing system.

Instead of sorting complete words alphabetically, you organize them character by character.

Start with:

A
B
C
...
Enter fullscreen mode Exit fullscreen mode

Under C:

CA
CB
CC
CD
...
Enter fullscreen mode Exit fullscreen mode

Under CA:

CAB
CAC
CAD
...
Enter fullscreen mode Exit fullscreen mode

Now suppose you want every word beginning with:

CAR
Enter fullscreen mode Exit fullscreen mode

You simply navigate:

C → A → R
Enter fullscreen mode Exit fullscreen mode

Once you reach CAR, everything below that point represents words beginning with CAR.

That is exactly what a Trie does.


Code Example

Let's implement a simple Trie in Java.

class TrieNode {

    TrieNode[] children = new TrieNode[26];

    boolean isEndOfWord;
}
Enter fullscreen mode Exit fullscreen mode

Each node contains 26 possible children:

a → index 0
b → index 1
c → index 2
...
z → index 25
Enter fullscreen mode Exit fullscreen mode

Now let's create the Trie:

class Trie {

    private TrieNode root;

    public Trie() {
        root = new TrieNode();
    }

    public void insert(String word) {

        TrieNode current = root;

        for (char c : word.toCharArray()) {

            int index = c - 'a';

            if (current.children[index] == null) {
                current.children[index] = new TrieNode();
            }

            current = current.children[index];
        }

        current.isEndOfWord = true;
    }

    public boolean search(String word) {

        TrieNode current = root;

        for (char c : word.toCharArray()) {

            int index = c - 'a';

            if (current.children[index] == null) {
                return false;
            }

            current = current.children[index];
        }

        return current.isEndOfWord;
    }
}
Enter fullscreen mode Exit fullscreen mode

We can use it like this:

public class Main {

    public static void main(String[] args) {

        Trie trie = new Trie();

        trie.insert("apple");
        trie.insert("app");
        trie.insert("apply");

        System.out.println(trie.search("app"));
        System.out.println(trie.search("apple"));
        System.out.println(trie.search("banana"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

true
true
false
Enter fullscreen mode Exit fullscreen mode

How insertion works

When inserting:

apple
Enter fullscreen mode Exit fullscreen mode

we follow:

root
 ↓
a
 ↓
p
 ↓
p
 ↓
l
 ↓
e
Enter fullscreen mode Exit fullscreen mode

At e, we set:

isEndOfWord = true;
Enter fullscreen mode Exit fullscreen mode

Now insert:

app
Enter fullscreen mode Exit fullscreen mode

The nodes:

a → p → p
Enter fullscreen mode Exit fullscreen mode

already exist.

We simply follow them and mark the p node as another word ending.

This is why prefixes are shared.


Prefix Search

One of the biggest advantages of a Trie is prefix searching.

Suppose we inserted:

apple
app
application
apply
banana
band
Enter fullscreen mode Exit fullscreen mode

We can ask:

Does any word start with "app"?
Enter fullscreen mode Exit fullscreen mode

We don't need to compare against every word.

We simply follow:

a → p → p
Enter fullscreen mode Exit fullscreen mode

If that path exists, the prefix exists.

A simple prefix-check method:

public boolean startsWith(String prefix) {

    TrieNode current = root;

    for (char c : prefix.toCharArray()) {

        int index = c - 'a';

        if (current.children[index] == null) {
            return false;
        }

        current = current.children[index];
    }

    return true;
}
Enter fullscreen mode Exit fullscreen mode

Now:

trie.startsWith("app");
Enter fullscreen mode Exit fullscreen mode

returns:

true
Enter fullscreen mode Exit fullscreen mode

while:

trie.startsWith("xyz");
Enter fullscreen mode Exit fullscreen mode

returns:

false
Enter fullscreen mode Exit fullscreen mode

Time Complexity

Let:

L = length of the word
Enter fullscreen mode Exit fullscreen mode

Then:

Operation Time Complexity
Insert O(L)
Search O(L)
Prefix Search O(L)
Delete O(L)

The important observation is that the complexity depends on the length of the string, not directly on the number of words stored.

For example, searching for:

application
Enter fullscreen mode Exit fullscreen mode

requires following roughly 11 characters regardless of whether the Trie contains:

100 words
Enter fullscreen mode Exit fullscreen mode

or:

1,000,000 words
Enter fullscreen mode Exit fullscreen mode

The actual performance also depends on the implementation and alphabet.


Common Mistakes

Mistake 1: Thinking every node represents a complete word

Consider:

car
cart
Enter fullscreen mode Exit fullscreen mode

The node representing:

car
Enter fullscreen mode Exit fullscreen mode

is shared by both words.

Therefore, we need:

isEndOfWord
Enter fullscreen mode Exit fullscreen mode

to distinguish between:

car
Enter fullscreen mode Exit fullscreen mode

and:

cart
Enter fullscreen mode Exit fullscreen mode

A node can be:

  • A prefix only
  • The end of a word
  • Both a prefix and the end of a word

Mistake 2: Assuming Tries are always memory efficient

Tries can consume significant memory.

If every node contains:

TrieNode[] children = new TrieNode[26];
Enter fullscreen mode Exit fullscreen mode

each node reserves space for 26 references.

For a large vocabulary, this can become expensive.

Alternative implementations can use:

Map<Character, TrieNode>
Enter fullscreen mode Exit fullscreen mode

instead of a fixed array.

This saves space when each node has only a few children, although hash-map overhead can also be significant.


Mistake 3: Ignoring case and character sets

The example implementation assumes:

a-z
Enter fullscreen mode Exit fullscreen mode

Only.

Real applications may need:

A-Z
0-9
Unicode
spaces
punctuation
Enter fullscreen mode Exit fullscreen mode

A production Trie therefore needs a carefully designed character representation.


Advanced Notes

1. Trie vs Hash Table

A hash table is excellent for:

Does this exact word exist?
Enter fullscreen mode Exit fullscreen mode

A Trie is excellent for:

Does this prefix exist?
What words begin with this prefix?
Enter fullscreen mode Exit fullscreen mode

For example:

Hash Table

apple → value
apply → value
banana → value
Enter fullscreen mode Exit fullscreen mode

Trie:

a
|
p
|
p
├── l → e
└── l → y
Enter fullscreen mode Exit fullscreen mode

The Trie explicitly represents the relationship between strings and their prefixes.


2. Trie vs Binary Search Tree

A Binary Search Tree organizes elements based on comparisons.

A Trie organizes strings based on their characters.

For example:

BST:
Compare complete strings

Trie:
Compare character by character
Enter fullscreen mode Exit fullscreen mode

This makes Tries particularly useful for prefix-based operations.


3. Autocomplete

Suppose a user types:

pro
Enter fullscreen mode Exit fullscreen mode

The application navigates to the node representing:

p → r → o
Enter fullscreen mode Exit fullscreen mode

Then it explores the subtree below that node.

It might find:

program
programming
programmer
project
process
Enter fullscreen mode Exit fullscreen mode

These become autocomplete suggestions.


4. Compressed Trie

A normal Trie can contain many nodes with only one child.

For example:

c
|
o
|
m
|
p
|
u
|
t
|
e
|
r
Enter fullscreen mode Exit fullscreen mode

A compressed Trie can combine chains of single-child nodes:

computer
Enter fullscreen mode Exit fullscreen mode

This reduces the number of nodes and can improve memory usage.

A compressed Trie is also commonly called a Radix Tree or Patricia Trie, depending on the specific variant.


5. Deletion

Deleting a word from a Trie requires care.

Suppose we have:

car
cart
Enter fullscreen mode Exit fullscreen mode

If we delete:

car
Enter fullscreen mode Exit fullscreen mode

we cannot necessarily delete the r node because:

cart
Enter fullscreen mode Exit fullscreen mode

still needs it.

Instead, we can simply change:

isEndOfWord = false;
Enter fullscreen mode Exit fullscreen mode

If the nodes are no longer needed by any other word, they can potentially be removed.


6. Unicode and Memory Optimization

For large-scale systems, a fixed array like:

new TrieNode[26]
Enter fullscreen mode Exit fullscreen mode

may not be appropriate.

Other approaches include:

HashMap
Sorted Map
Compressed Trie
Ternary Search Tree
Memory-mapped structures
Enter fullscreen mode Exit fullscreen mode

The correct choice depends on the alphabet, dataset size, access patterns, and memory constraints.


The Bigger Picture

Look at how our data structures have evolved:

Arrays
   ↓
Linked Lists
   ↓
Stacks / Queues
   ↓
Hash Tables
   ↓
Trees
   ↓
Heaps
   ↓
Graphs
   ↓
Tries
Enter fullscreen mode Exit fullscreen mode

Each structure solves a different kind of problem.

A Hash Table gives us fast exact lookup.

A Heap gives us efficient priority-based access.

A Graph represents relationships.

A Trie represents strings and their prefixes.

The interesting part is that a Trie is actually built on a familiar idea:

Trie = Tree + Character-based navigation
Enter fullscreen mode Exit fullscreen mode

This is an important connection.

We learned that trees allow one node to have multiple children.

A Trie takes that idea and uses each level to represent another character.


The Most Important Mental Model

Remember this:

A Trie is a tree where each path represents a string, and shared paths represent shared prefixes.

For example:

        root
          |
          c
          |
          a
       /  |  \
      t   r   n
Enter fullscreen mode Exit fullscreen mode

The path:

root → c → a → t
Enter fullscreen mode Exit fullscreen mode

represents:

cat
Enter fullscreen mode Exit fullscreen mode

The path:

root → c → a → r
Enter fullscreen mode Exit fullscreen mode

represents:

car
Enter fullscreen mode Exit fullscreen mode

The shared path:

c → a
Enter fullscreen mode Exit fullscreen mode

represents their common prefix.

If you remember "tree of characters", you understand the core idea of a Trie.


Summary

A Trie is a tree-based data structure designed primarily for storing and searching strings.

Key ideas:

  • Each node represents a character.
  • A path from the root represents a string.
  • Common prefixes are shared.
  • isEndOfWord identifies complete words.
  • Search takes O(L), where L is the string length.
  • Prefix searches are one of its biggest strengths.
  • Tries are useful for autocomplete and dictionary-like applications.
  • They can consume significant memory.
  • Compressed Tries can reduce memory usage.

The most important distinction is:

Hash Table → Exact lookup

Trie → Prefix-aware lookup
Enter fullscreen mode Exit fullscreen mode

Top comments (0)