datarekha

Tries (Prefix Trees)

A tree where the path from the root spells a string — giving O(L) insert and lookup that never depends on how many words you store, and making prefix search trivially fast.

7 min read Intermediate Data Structures & Algorithms Lesson 18 of 32

What you'll learn

  • Why a trie stores characters along the path, so words with a shared prefix share structure
  • Why insert and lookup are O(L) — the word length — not O(dictionary size)
  • How prefix search powers autocomplete almost for free
  • Where tries show up — autocomplete, IP routing, spell-check, tokenizer vocabularies

Before you start

A trie — said “try”, from retrieval — is a tree where the path from the root spells out a string.

To store "cat", you walk down from the root along c, then a, then t, and mark that last spot as the end of a word. To store "car", you walk c, a, then r. Notice what just happened: the c → a part of the path was already there from "cat", so the two words share it. That shared-prefix sharing is the entire idea of a trie, and everything good about it follows from there.

One picture

Here is the trie after inserting cat, car, card, do, and dog. Green nodes mark where a complete word ends:

catrddogcatcarcarddodog
The c → a prefix is stored once and shared by cat, car, and card. And “do” is a complete word that also sits on the way to “dog”.

A node can be both an end-of-word and keep going: do ends at the o, yet dog continues through that same o. The root stands for the empty prefix; each step down adds one character.

Why the cost ignores the dictionary

Let L be the length of a word. Then insert is O(L), exact lookup is O(L), and prefix search is O(L) to reach the prefix plus O(k) to gather its k completions.

The striking thing is what is missing from those costs: the number of words in the trie. A lookup never compares your query against other words — it just follows one edge per character, steered entirely by the query itself. Whether the trie holds ten words or ten million, looking up an 8-letter word walks exactly 8 edges. A hash set also gives O(1) lookup, but it has no structure to exploit for “every word starting with ca” — it would have to scan every key. A trie answers that with a single subtree walk. The trade is memory: a trie spends nodes to buy cheap prefix queries.

Building one in Python

The cleanest version is a dictionary of dictionaries — each node maps a character to its child node, and a sentinel key marks the end of a word:

class Trie:
    def __init__(self):
        self.root = {}

    def insert(self, word):
        node = self.root
        for ch in word:
            node = node.setdefault(ch, {})   # walk, creating nodes as needed
        node["$"] = True                     # mark end-of-word

    def search(self, word):
        node = self.root
        for ch in word:
            if ch not in node:
                return False
            node = node[ch]
        return "$" in node                   # reached the end — but is it a word?

    def startswith(self, prefix):
        node = self.root
        for ch in prefix:
            if ch not in node:
                return False
            node = node[ch]
        return True                          # reached the prefix — a subtree exists

t = Trie()
for w in ["cat", "car", "card", "do", "dog"]:
    t.insert(w)

print(t.search("car"))       # a stored word
print(t.search("ca"))        # only a prefix, not a word
print(t.startswith("ca"))    # but words do start with it
print(t.startswith("dx"))    # nothing starts with "dx"
True
False
True
False

The "$" sentinel works because it can never be a real character edge, so it cleanly distinguishes “a word ends here” from “the path merely continues.” (A dedicated is_end flag on a node class does the same job, a touch more tidily.)

When a trie beats a hash map

Reach for a trie when prefix queries matter — autocomplete, type-ahead, URL routing — or when a shared prefix is worth compressing (50,000 words starting with un store that prefix once, not 50,000 times), or when you want alphabetical order for free (a depth-first walk of a trie visits words sorted). Reach for a plain hash set when you only ever need exact-match lookup and memory is tight.

Practice

Quick check

0/3
Q1A trie holds 100,000 words. How long does it take to look up a single 8-character word?
Q2Which operation does a trie do far better than a hash set?
Q3You insert 'cat' then 'car' into an empty trie. How many nodes exist in total, including the root?

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
Group a list of words into anagrams.

Sort each word's characters to get a canonical key, then bucket words by that key using a hash map. This turns an O(n²) brute-force comparison into a clean O(n · k log k) single pass, where k is the max word length.

Generate all subsets of a set — the power set — using backtracking.

At each step of a recursive walk through the input, you make a binary choice: include the current element or skip it. Recording the current path at every node of the recursion tree (not just the leaves) collects all 2^n subsets. A `start` index prevents duplicates by ensuring elements are only considered left-to-right.

Find all unique combinations of candidates that sum to a target, where each candidate may be used an unlimited number of times.

Use backtracking with a running total. At each step, try adding a candidate to the current path. If the total equals the target, record the path. If it exceeds the target, prune. Passing the same start index (not i+1) back into the recursion allows unlimited reuse of the same element.

How does a B-tree index work, and when does the database choose not to use it?

A B-tree index stores key values in a balanced tree of sorted nodes, allowing the engine to reach any value in O(log n) page reads instead of scanning every row. The optimizer skips the index when the estimated cost of random I/O exceeds a full-table scan, when a function wraps the indexed column, or when the query returns such a large fraction of rows that a sequential scan is cheaper.

Related lessons

Explore further

Skip to content