<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. DSA
  3. Trees
All lessons in this course

Lesson 30 of 50 13 minIntermediate

Tries (prefix trees)

Store words letter by letter in a tree so that lookups and prefix searches cost O(length of the word), build autocomplete, and know when a trie beats a HashSet.

Type “bo” into a search box and it suggests “bolt” and “bolts”. A HashSet<string> can tell you instantly whether “bolt” exists, but it cannot answer “which words start with bo?” without checking every word. A trie (from retrieval, usually said “try”) answers prefix questions directly.

One letter per edge

The root represents the empty string. Each edge adds one letter, so every node represents the prefix spelled by the path to it. A node is marked when a complete word ends there. Words that share a beginning share that part of the path: “beam” and “bear” share b-e-a, and “bolt” is a marked node on the way to “bolts”.

Interactive

A trie (prefix tree)

Each edge is one letter. A word is the path from the root to a node marked with a double ring. Words that start the same share the start of their path.

mraestloecarbnmuloc
Followed 2 letters from the root. Everything below that node starts with “bo”: bolt, bolts.
Words 6 · Letters in all words 28 · Trie nodes 19 (shared prefixes are stored once)

A trie in C#

Each node holds a dictionary from letter to child node, and a flag for “a word ends here”. Insert walks the word, creating missing nodes. Lookup walks the word and fails as soon as a letter is missing.

Trie.cs
using System.Text; var trie = new Trie();foreach (var w in new[] { "beam", "bear", "bolt", "bolts", "brace", "column" })    trie.Insert(w); Console.WriteLine(trie.Contains("bolt"));Console.WriteLine(trie.Contains("bea"));       // only a prefix, not a wordConsole.WriteLine(trie.StartsWith("bea"));Console.WriteLine(string.Join(", ", trie.WordsWithPrefix("b"))); class Trie{    private class Node    {        public Dictionary<char, Node> Children { get; } = new();        public bool IsWord;    }    private readonly Node _root = new();     public void Insert(string word)    {        var n = _root;        foreach (char c in word)        {            if (!n.Children.TryGetValue(c, out var next))                n.Children[c] = next = new Node();     // create the missing step            n = next;        }        n.IsWord = true;    }     private Node? Find(string prefix)    {        var n = _root;        foreach (char c in prefix)        {            if (!n.Children.TryGetValue(c, out var next)) return null;   // path breaks off            n = next;        }        return n;    }     public bool Contains(string word) => Find(word)?.IsWord == true;    public bool StartsWith(string prefix) => Find(prefix) is not null;     public List<string> WordsWithPrefix(string prefix)    {        var result = new List<string>();        var start = Find(prefix);        if (start is not null) Collect(start, new StringBuilder(prefix), result);        return result;    }     private static void Collect(Node n, StringBuilder sb, List<string> result)    {        if (n.IsWord) result.Add(sb.ToString());        foreach (var (c, child) in n.Children.OrderBy(p => p.Key))   // alphabetical        {            sb.Append(c);            Collect(child, sb, result);            sb.Length--;                                              // undo the letter        }    }}
Output
True
False
True
beam, bear, bolt, bolts, brace

How fast is it?

Insert, lookup and prefix check cost O(L), where L is the length of the word or prefix, no matter how many words are stored. Listing all words with a prefix costs O(L) to reach the prefix node, plus the size of that subtree. A hash set lookup is also O(L) in practice (computing the hash reads every character), so tries do not win on plain lookups. They win on everything to do with prefixes.

QuestionHashSet<string>Sorted array + binary searchTrie
Is “bolt” a word?O(L), fastestO(L log n)O(L)
Any word starting with “bo”?O(n · L): check allO(L log n)O(L)
All words starting with “bo”O(n · L)O(L log n + k)O(L + size of answer)
Memorylowlowesthigh: a node per letter

Where tries are used

  • Autocomplete and search suggestions, spell checkers (“did you mean…”).
  • Routing tables: an internet router finds the longest matching prefix of an IP address, stored bit by bit in a binary trie.
  • Word games and puzzles: stop searching a path the moment no word starts with the letters so far.
  • Prefix codes: Huffman compression decodes bits by walking a binary trie.
Quick check

A trie holds 100,000 words. How many steps does StartsWith("colu") take?

Quick check

Why does each trie node need an “is a word” flag?

Practice

Build a CountingTrie where CountWithPrefix(string prefix) returns how many inserted words start with prefix, in O(length of the prefix), without walking the subtree. Words: beam, bear, bolt, bolts, brace. Expected: b → 5, bo → 2, bea → 2, c → 0. (Assume each word is inserted once.)

Show a hint

Store a counter in every node: how many words pass through it. Increment it for each node on the path during Insert. The answer is the counter of the node at the end of the prefix.

Show the solution
Solution.cs
var t = new CountingTrie();foreach (var w in new[] { "beam", "bear", "bolt", "bolts", "brace" }) t.Insert(w);foreach (var p in new[] { "b", "bo", "bea", "c" })    Console.WriteLine($"{p}: {t.CountWithPrefix(p)}"); class CountingTrie{    private class Node    {        public Dictionary<char, Node> Children { get; } = new();        public int Count;                           // words that pass through this node    }    private readonly Node _root = new();     public void Insert(string word)    {        var n = _root;        foreach (char c in word)        {            if (!n.Children.TryGetValue(c, out var next))                n.Children[c] = next = new Node();            next.Count++;            n = next;        }    }     public int CountWithPrefix(string prefix)    {        var n = _root;        foreach (char c in prefix)        {            if (!n.Children.TryGetValue(c, out var next)) return 0;            n = next;        }        return n.Count;    }}
Output
b: 5
bo: 2
bea: 2
c: 0

Remember

  • A trie stores strings letter by letter; each node is a prefix, shared by all words that start with it.
  • Insert, lookup and prefix checks are O(length of the word), independent of how many words are stored.
  • Mark word ends explicitly: a word can end in the middle of a longer word’s path.
  • Tries shine for prefix queries (autocomplete, routing) but use more memory than a HashSet.
0 of 50 lessons completed