Skip to content

Expert: advanced patterns and interviews · Tries, union-find and bit manipulation · topic 47 of 61 · 35 min

Tries (prefix trees)

Explanation

A trie (prefix tree) stores words letter by letter: each node has a map of children and a flag that marks the end of a word. Insert, search and "does any word start with this prefix?" all take O(L) time for a word of length L, no matter how many words are stored. Tries power search-box suggestions and contact search, at the cost of extra memory.

The problem: questions about prefixes

When you type "ban" into a shopping app's search box, it suggests "banana", "bangles" and "bandage" before you finish. The question is not "is this exact word stored?" but "which stored words START with these letters?". A set answers the first question in O(L) time (hashing a word of length L), but it cannot answer the second without checking every word. With N words, scanning costs O(N × L) per keystroke.

What a trie is

A trie (from "retrieval", usually said like "try") is a tree where each edge stands for one character. The path from the root to a node spells a prefix. Each node has a children map (a dictionary from character to child node) and an end flag (often called is_end) that says "a complete word ends here".

Words that share a prefix share the same path. After inserting "car", "cat" and "do", the root has two children, c and d. The path c → a is shared by "car" and "cat", and only then splits into r and t.

output
(root)
 ├─ c
 │   └─ a
 │       ├─ r   (end of "car")
 │       └─ t   (end of "cat")
 └─ d
     └─ o       (end of "do")
The trie for "car", "cat" and "do". Shared prefixes are stored once.
python
class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_end = False

root = TrieNode()

def insert(word):
    node = root
    for ch in word:
        if ch not in node.children:
            node.children[ch] = TrieNode()
        node = node.children[ch]
    node.is_end = True

for w in ["car", "cat", "do"]:
    insert(w)
print(sorted(root.children))                              # Output: ['c', 'd']
print(sorted(root.children["c"].children["a"].children))  # Output: ['r', 't']
Insert walks down one character at a time and creates missing nodes.

Search versus starts_with

Both operations walk down from the root, one character at a time. If a character is missing, the answer is False. The difference is at the end. search(word) needs is_end to be True at the last node, because "ca" is a path in the trie above but not a stored word. starts_with(prefix) only needs the walk to succeed: "ca" is a prefix of "car".

Cost

Insert, search and starts_with each visit at most L nodes for a word or prefix of length L, so each is O(L) time, and the number of stored words does not matter. Space is the catch. In the worst case every character of every word is its own node, so a trie holds up to O(total characters) nodes, and in Python each node is an object with its own dictionary. A trie of a million short words can use many times the memory of a set holding the same words. Compact versions use an array of 26 children for lowercase letters, or merge chains of single children into one edge (a radix tree).

Autocomplete

To suggest words for a prefix, first walk down to the prefix node (O(L)). Then run a DFS below it and collect every node whose is_end is True, building the word as you go. If you visit the children in sorted order, the words come out in alphabetical order, so you can stop as soon as you have k of them. Real search boxes rank suggestions by popularity or by what you searched before, often by keeping the top few suggestions at each node, but the walk-then-collect idea is the same.

Longest common prefix

The longest common prefix of a list of words is the longest start that all of them share: "fl" for "flower", "flow" and "flight". With a trie, insert all the words and walk down from the root while the current node has exactly one child and is not the end of a word. Without a trie there is a neat trick: the common prefix of all the words equals the common prefix of the alphabetically smallest and largest words.

python
words = ["flower", "flow", "flight"]
a, b = min(words), max(words)
i = 0
while i < len(a) and a[i] == b[i]:
    i += 1
print(a[:i])   # Output: fl
min is "flight" and max is "flower"; every other word sorts between them, so it shares their common start.

Common mistakes

  • Forgetting the end flag, so search("ca") returns True after inserting only "car". Fix: set is_end at the last node of insert and check it in search.
  • Using a mutable default argument such as def __init__(self, children={}), which makes every node share ONE dictionary. Fix: create self.children = {} inside __init__.
  • Storing the trie as a class attribute, so two Trie objects share the same words. Fix: create the root in __init__.
  • Collecting autocomplete results without sorting the children, then sorting a huge list at the end. Fix: visit children in sorted order and stop at k.
  • Using a trie when only exact lookups are needed. Fix: a set is simpler and smaller.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Tries (prefix trees) · 1/8

Tries

This lesson is about tries, also called prefix trees: the structure behind search suggestions.

Prefer a video by a teacher? Find free videos on “trie data structure implement insert search startswith autocomplete” on YouTube ↗

In real life: Contact search on a phone

When you type "Ra" in your phone's contacts, the list shrinks to names starting with those letters. A trie keyed by letters is one way to do this: each keystroke moves one level down, and the names below the current node are the matches. Real apps also match surnames, nicknames and phone numbers, and rank frequent contacts first, but prefix lookup is the core.

python
class Node:
    def __init__(self):
        self.children = {}
        self.is_end = False

root = Node()
for name in ["rahul", "rajesh", "ramya", "ravi", "rekha", "sana"]:
    node = root
    for ch in name:
        node = node.children.setdefault(ch, Node())
    node.is_end = True

def names_below(node, path, out):
    if node.is_end:
        out.append(path)
    for ch in sorted(node.children):
        names_below(node.children[ch], path + ch, out)

node = root
for ch in "ra":
    node = node.children[ch]
found = []
names_below(node, "ra", found)
print(found)
Shows ['rahul', 'rajesh', 'ramya', 'ravi']: rekha and sana do not start with "ra".

Practice (0/3 done)

Task 1 · Code · Python

Write a class Trie with three methods. insert(word) adds a word (returns None). search(word) returns True only if that exact word was inserted. starts_with(prefix) returns True if some inserted word starts with prefix; starts_with("") is always True. Each trie must keep its own words. Example: after insert("apple"), search("app") is False and starts_with("app") is True.

Task 2 · Code · Python

Write autocomplete(words, prefix, k) that returns up to k DIFFERENT words from words that start with prefix, in alphabetical order. Build a trie, walk to the prefix node, then collect words with a DFS that visits children in sorted order and stops after k. Example: autocomplete(["mobile", "mouse", "moneypot", "monitor", "mousepad"], "mou", 5) returns ["mouse", "mousepad"].

Task 3 · Code · Python

Write longest_common_prefix(words) that returns the longest string that every word in the list starts with. Return "" for an empty list or when the words share nothing. Example: longest_common_prefix(["flower", "flow", "flight"]) returns "fl".

Quick quiz

Question 1

1.A trie holds N words. What does search(word) cost for a word of length L?

Question 2

2.Why does each trie node need an end flag?

Question 3

3.You insert "tea" and "ten" into an empty trie. How many nodes are there, counting the root?

Question 4

4.Which question does a plain set answer just as well as a trie?

Question 5

5.What is the main cost of a trie compared with a set of the same words?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

A trie holds N words. What does search(word) cost for a word of length L?

Answer: O(L)

It walks at most L nodes; the number of stored words does not matter.

Why does each trie node need an end flag?

Answer: To tell a complete word apart from a prefix of a longer word

After inserting "car", the path c-a exists, but "ca" is not a word unless its node is marked.

You insert "tea" and "ten" into an empty trie. How many nodes are there, counting the root?

Answer: 5

root, t, e, then a and n: the prefix "te" is shared.

Which question does a plain set answer just as well as a trie?

Answer: Is "chai" one of the stored words?

Exact membership is O(L) in a set too; prefix questions are where tries win.

What is the main cost of a trie compared with a set of the same words?

Answer: More memory, since every node is an object with its own map

Many small node objects add up; compressed tries reduce this.

Why is searching down one path O(log n) in a balanced tree but O(n) in a skewed one?Related

Answer: The cost is O(height), and the height is about log n when balanced but n − 1 when skewed

Walking down one path costs one step per level, and the number of levels is the height.

From Trees: words and shapes

How much extra space does "first unique character" need for a string of lowercase English letters?Related

Answer: O(1), at most 26 counts

The count map has at most one entry per letter, and there are only 26 letters.

From Counting and grouping with hashing

n items and q range-sum queries. What is the total cost with a prefix array?Related

Answer: O(n + q)

O(n) to build once, then O(1) per query.

From Prefix sums and difference arrays

What is the worst-case search time in an unbalanced BST?Related

Answer: O(n)

A chain must be walked node by node.

From Binary search trees

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 4 more topics in “Tries, union-find and bit manipulation” before its exam: Union-find (disjoint set union), Minimum spanning trees, Bits: AND, OR, XOR and shifts, Bit tricks in interviews.
  2. Module 10 exam: Tries, union-find and bits · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Dynamic programming. 61 topics left in the course before the final exam.

← Previous: Graph problem patterns