Prefix problems
The classic things a trie answers: autocomplete, longest common prefix, prefix counts, ranked suggestions and add-and-search dictionaries.
Learning goals
- List every word that starts with a prefix, in alphabetical order
- Compute the longest common prefix of a set of words
- Answer 'how many words start with p' in O(length of p) using pass counts
- Build ranked suggestions and a dictionary that supports a '.' wildcard
Prerequisites
Search boxes, phone keyboards, spell checkers and IP routing tables all ask the same question: what do I know that begins with this? A trie answers by walking to the prefix and reading off everything below it.
Autocomplete
Autocomplete has two steps: walk to the node for the typed prefix, then collect the words in the subtree below it. If the children are visited in alphabetical order, the words come out alphabetically for free, with no sort. The cost is O(length of the prefix + size of the subtree), and a limit stops the walk early when only the first few suggestions are needed.
Run the animation with the question "Autocomplete: every word with the prefix" and the prefix ca: it finds car, cart and cat. With the preset "Prefix with no match", the walk breaks at the x of cx and returns nothing.
1class Trie {2private static class Node {3Node[] next = new Node[26];4boolean end;5}6private final Node root = new Node();7void insert(String word) {8Node n = root;9for (char c : word.toCharArray()) {10int i = c - 'a';11if (n.next[i] == null) n.next[i] = new Node();12n = n.next[i];13}14n.end = true;15}16List<String> autocomplete(String prefix, int limit) {17List<String> out = new ArrayList<>();18Node n = root;19for (char c : prefix.toCharArray()) {20n = n.next[c - 'a'];21if (n == null) return out; // nothing starts with this prefix22}23collect(n, new StringBuilder(prefix), out, limit);24return out;25}26private void collect(Node n, StringBuilder sb, List<String> out, int limit) {27if (out.size() >= limit) return;28if (n.end) out.add(sb.toString()); // a word ends here29for (int c = 0; c < 26 && out.size() < limit; c++) { // alphabetical order for free30if (n.next[c] == null) continue;31sb.append((char) ('a' + c));32collect(n.next[c], sb, out, limit);33sb.deleteCharAt(sb.length() - 1);34}35}36}
Prefix queries: autocomplete, prefix counts, longest common prefix
Interactive · edit the inputtrie · 9 nodes
word
word
word
word
word
Autocomplete = walk to the prefix node, then collect every word in its subtree in alphabetical order.
1private Node walk(String s) { // node for a prefix, or null2Node n = root;3for (char c : s.toCharArray()) { n = n.next[c - 'a']; if (n == null) return null; }4return n;5}6int countPrefix(String p) { Node n = walk(p); return n == null ? 0 : n.pass; } // insert did pass++ on every node it visits7List<String> autocomplete(String p) {8List<String> out = new ArrayList<>();9Node n = walk(p);10if (n != null) collect(n, new StringBuilder(p), out);11return out;12}13private void collect(Node n, StringBuilder sb, List<String> out) {14if (n.end) out.add(sb.toString()); // a word ends here15for (int c = 0; c < 26; c++) { // alphabetical order for free16if (n.next[c] == null) continue;17sb.append((char) ('a' + c));18collect(n.next[c], sb, out);19sb.deleteCharAt(sb.length() - 1);20}21}22String longestCommonPrefix() {23StringBuilder sb = new StringBuilder();24Node n = root;25while (!n.end && n.kids == 1) { // one way forward and no word stops here26int c = onlyChild(n);27sb.append((char) ('a' + c));28n = n.next[c];29}30return sb.toString();31}
Live state
words
5
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | words=5 | Autocomplete = walk to the prefix node, then collect every word in its subtree in alphabetical order. |
| 2 | L3 | prefix="c" pass=3 | Follow 'c': 3 words run through this node. |
| 3 | L3 | prefix="ca" pass=3 | Follow 'a': 3 words run through this node. |
| 4 | L15 | path="ca" | Visit "ca": not a word, keep going down. |
| 5 | L14 | word="car" found=1 | A word ends here: add "car" to the result. |
| 6 | L14 | word="cart" found=2 | A word ends here: add "cart" to the result. |
| 7 | L14 | word="cat" found=3 | A word ends here: add "cat" to the result. |
| 8 | L11 | return=["car", "cart", "cat"] | 3 completions for "ca": "car", "cart", "cat". Cost O(length of the prefix + size of the subtree). |
Common prefixes
The longest common prefix of a set of words is the path from the root that every word follows. Walk down from the root while the current node has exactly one child and no word ends there: the first node with two or more children (the words diverge) or with an end flag (a word stops) marks the end of the common prefix.
Choose the preset "Longest common prefix" (flower flow flight): the path f, l is the only way until the node fl splits into o and i, so the answer is fl. With dog car race the root already has three children and the answer is empty. For a one-off question a direct comparison of the strings is simpler; the trie pays off when the same word set is queried repeatedly.
1class LcpTrie {2private static class Node {3Node[] next = new Node[26];4boolean end;5int kids;6}7private final Node root = new Node();8void insert(String word) {9Node n = root;10for (char c : word.toCharArray()) {11int i = c - 'a';12if (n.next[i] == null) { n.next[i] = new Node(); n.kids++; }13n = n.next[i];14}15n.end = true;16}17String longestCommonPrefix() {18StringBuilder sb = new StringBuilder();19Node n = root;20while (!n.end && n.kids == 1) { // one way forward and no word stops here21int c = 0;22while (n.next[c] == null) c++; // the only child23sb.append((char) ('a' + c));24n = n.next[c];25}26return sb.toString();27}28}
Prefix queries: autocomplete, prefix counts, longest common prefix
Interactive · edit the inputtrie · 9 nodes
word
word
word
word
word
Autocomplete = walk to the prefix node, then collect every word in its subtree in alphabetical order.
1private Node walk(String s) { // node for a prefix, or null2Node n = root;3for (char c : s.toCharArray()) { n = n.next[c - 'a']; if (n == null) return null; }4return n;5}6int countPrefix(String p) { Node n = walk(p); return n == null ? 0 : n.pass; } // insert did pass++ on every node it visits7List<String> autocomplete(String p) {8List<String> out = new ArrayList<>();9Node n = walk(p);10if (n != null) collect(n, new StringBuilder(p), out);11return out;12}13private void collect(Node n, StringBuilder sb, List<String> out) {14if (n.end) out.add(sb.toString()); // a word ends here15for (int c = 0; c < 26; c++) { // alphabetical order for free16if (n.next[c] == null) continue;17sb.append((char) ('a' + c));18collect(n.next[c], sb, out);19sb.deleteCharAt(sb.length() - 1);20}21}22String longestCommonPrefix() {23StringBuilder sb = new StringBuilder();24Node n = root;25while (!n.end && n.kids == 1) { // one way forward and no word stops here26int c = onlyChild(n);27sb.append((char) ('a' + c));28n = n.next[c];29}30return sb.toString();31}
Live state
words
5
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | words=5 | Autocomplete = walk to the prefix node, then collect every word in its subtree in alphabetical order. |
| 2 | L3 | prefix="c" pass=3 | Follow 'c': 3 words run through this node. |
| 3 | L3 | prefix="ca" pass=3 | Follow 'a': 3 words run through this node. |
| 4 | L15 | path="ca" | Visit "ca": not a word, keep going down. |
| 5 | L14 | word="car" found=1 | A word ends here: add "car" to the result. |
| 6 | L14 | word="cart" found=2 | A word ends here: add "cart" to the result. |
| 7 | L14 | word="cat" found=3 | A word ends here: add "cat" to the result. |
| 8 | L11 | return=["car", "cart", "cat"] | 3 completions for "ca": "car", "cart", "cat". Cost O(length of the prefix + size of the subtree). |
Prefix counts
To count the words that start with a prefix, keep a pass counter on every node: the number of words whose path goes through it. insert increments the counter of each node it visits, so a prefix count is one lookup: walk to the prefix node and read its counter. The answer does not depend on how many words match.
Choose the question "How many words start with the prefix" with the preset "Count words with prefix": in apple app apt art the prefix ap has the node with pass 3. The counts appear as ×n under each letter. Counting with a plain DFS would cost the size of the subtree; the stored counter makes it O(length of the prefix).
1class PrefixCounter {2private static class Node {3Node[] next = new Node[26];4int pass; // words that go through this node5int ends; // words that end exactly here6}7private final Node root = new Node();8void insert(String word) {9Node n = root;10for (char c : word.toCharArray()) {11int i = c - 'a';12if (n.next[i] == null) n.next[i] = new Node();13n = n.next[i];14n.pass++;15}16n.ends++;17}18int countPrefix(String prefix) { // words that start with prefix19Node n = root;20for (char c : prefix.toCharArray()) {21n = n.next[c - 'a'];22if (n == null) return 0;23}24return n.pass;25}26int countWords(String word) { // words equal to word27Node n = root;28for (char c : word.toCharArray()) {29n = n.next[c - 'a'];30if (n == null) return 0;31}32return n.ends;33}34}
Prefix queries: autocomplete, prefix counts, longest common prefix
Interactive · edit the inputtrie · 9 nodes
word
word
word
word
word
Autocomplete = walk to the prefix node, then collect every word in its subtree in alphabetical order.
1private Node walk(String s) { // node for a prefix, or null2Node n = root;3for (char c : s.toCharArray()) { n = n.next[c - 'a']; if (n == null) return null; }4return n;5}6int countPrefix(String p) { Node n = walk(p); return n == null ? 0 : n.pass; } // insert did pass++ on every node it visits7List<String> autocomplete(String p) {8List<String> out = new ArrayList<>();9Node n = walk(p);10if (n != null) collect(n, new StringBuilder(p), out);11return out;12}13private void collect(Node n, StringBuilder sb, List<String> out) {14if (n.end) out.add(sb.toString()); // a word ends here15for (int c = 0; c < 26; c++) { // alphabetical order for free16if (n.next[c] == null) continue;17sb.append((char) ('a' + c));18collect(n.next[c], sb, out);19sb.deleteCharAt(sb.length() - 1);20}21}22String longestCommonPrefix() {23StringBuilder sb = new StringBuilder();24Node n = root;25while (!n.end && n.kids == 1) { // one way forward and no word stops here26int c = onlyChild(n);27sb.append((char) ('a' + c));28n = n.next[c];29}30return sb.toString();31}
Live state
words
5
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | words=5 | Autocomplete = walk to the prefix node, then collect every word in its subtree in alphabetical order. |
| 2 | L3 | prefix="c" pass=3 | Follow 'c': 3 words run through this node. |
| 3 | L3 | prefix="ca" pass=3 | Follow 'a': 3 words run through this node. |
| 4 | L15 | path="ca" | Visit "ca": not a word, keep going down. |
| 5 | L14 | word="car" found=1 | A word ends here: add "car" to the result. |
| 6 | L14 | word="cart" found=2 | A word ends here: add "cart" to the result. |
| 7 | L14 | word="cat" found=3 | A word ends here: add "cat" to the result. |
| 8 | L11 | return=["car", "cart", "cat"] | 3 completions for "ca": "car", "cart", "cat". Cost O(length of the prefix + size of the subtree). |
Contact directory
A contact directory or search-suggestion box shows a few ranked results after every keystroke. A common rule is: the three lexicographically smallest products that start with what was typed so far. Insert the products in sorted order and let every node remember the first three words that passed through it; then each keystroke is one edge followed and a stored list read.
Inserting in sorted order is what makes 'first three seen' equal to 'three smallest'. The total work is O(total length of all products) to build and O(length of the typed text) to answer all keystrokes, plus the size of the answers.
1static List<List<String>> suggestedProducts(String[] products, String typed) {2class Node {3Node[] next = new Node[26];4List<String> top = new ArrayList<>(); // the first three words that pass through this node5}6String[] sorted = products.clone();7Arrays.sort(sorted); // so the first three seen are the three smallest8Node root = new Node();9for (String w : sorted) {10Node n = root;11for (char c : w.toCharArray()) {12int i = c - 'a';13if (n.next[i] == null) n.next[i] = new Node();14n = n.next[i];15if (n.top.size() < 3) n.top.add(w);16}17}18List<List<String>> out = new ArrayList<>();19Node n = root;20for (char c : typed.toCharArray()) {21if (n != null) n = n.next[c - 'a'];22out.add(n == null ? new ArrayList<>() : new ArrayList<>(n.top));23}24return out;25}
Prefix queries: autocomplete, prefix counts, longest common prefix
Interactive · edit the inputtrie · 9 nodes
word
word
word
word
word
Autocomplete = walk to the prefix node, then collect every word in its subtree in alphabetical order.
1private Node walk(String s) { // node for a prefix, or null2Node n = root;3for (char c : s.toCharArray()) { n = n.next[c - 'a']; if (n == null) return null; }4return n;5}6int countPrefix(String p) { Node n = walk(p); return n == null ? 0 : n.pass; } // insert did pass++ on every node it visits7List<String> autocomplete(String p) {8List<String> out = new ArrayList<>();9Node n = walk(p);10if (n != null) collect(n, new StringBuilder(p), out);11return out;12}13private void collect(Node n, StringBuilder sb, List<String> out) {14if (n.end) out.add(sb.toString()); // a word ends here15for (int c = 0; c < 26; c++) { // alphabetical order for free16if (n.next[c] == null) continue;17sb.append((char) ('a' + c));18collect(n.next[c], sb, out);19sb.deleteCharAt(sb.length() - 1);20}21}22String longestCommonPrefix() {23StringBuilder sb = new StringBuilder();24Node n = root;25while (!n.end && n.kids == 1) { // one way forward and no word stops here26int c = onlyChild(n);27sb.append((char) ('a' + c));28n = n.next[c];29}30return sb.toString();31}
Live state
words
5
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | words=5 | Autocomplete = walk to the prefix node, then collect every word in its subtree in alphabetical order. |
| 2 | L3 | prefix="c" pass=3 | Follow 'c': 3 words run through this node. |
| 3 | L3 | prefix="ca" pass=3 | Follow 'a': 3 words run through this node. |
| 4 | L15 | path="ca" | Visit "ca": not a word, keep going down. |
| 5 | L14 | word="car" found=1 | A word ends here: add "car" to the result. |
| 6 | L14 | word="cart" found=2 | A word ends here: add "cart" to the result. |
| 7 | L14 | word="cat" found=3 | A word ends here: add "cat" to the result. |
| 8 | L11 | return=["car", "cart", "cat"] | 3 completions for "ca": "car", "cart", "cat". Cost O(length of the prefix + size of the subtree). |
Word dictionary
A word dictionary supports addWord(word) and search(pattern), where the pattern may contain . meaning 'any one letter'. A letter follows one edge; a dot tries every child, so the search becomes a depth-first search that returns true on the first match and backtracks on a dead end.
A pattern with no dots costs O(L), like a normal lookup. Each dot can multiply the work by the number of children, so in the worst case the search visits every node, but never more nodes than the trie has. Run the animation's "Dot in the middle" preset (b.d) and compare it with "No match" (b.t).
1class WordDictionary {2private static class Node {3Node[] next = new Node[26];4boolean end;5}6private final Node root = new Node();7void addWord(String word) {8Node n = root;9for (char c : word.toCharArray()) {10int i = c - 'a';11if (n.next[i] == null) n.next[i] = new Node();12n = n.next[i];13}14n.end = true;15}16boolean search(String pattern) { return search(root, pattern, 0); }17private boolean search(Node n, String p, int i) { // does any word below n match p[i..]?18if (n == null) return false;19if (i == p.length()) return n.end;20char c = p.charAt(i);21if (c != '.') return search(n.next[c - 'a'], p, i + 1); // a letter: one way only22for (Node child : n.next) // a dot: try every branch23if (child != null && search(child, p, i + 1)) return true;24return false;25}26}
Wildcards: '.' search by DFS, and the prefix + suffix filter
Interactive · edit the inputtrie · 13 nodes
word
word
word
word
Search "b.d". A letter follows exactly one edge; a dot tries every child in turn, so the search becomes a DFS on the trie.
1boolean search(Node n, String p, int i) { // does any word below n match p[i..]?2if (n == null) return false;3if (i == p.length()) return n.end;4char c = p.charAt(i);5if (c != '.') return search(n.next[c - 'a'], p, i + 1); // a letter: one way only6for (Node child : n.next) // a dot: try every branch7if (child != null && search(child, p, i + 1)) return true;8return false;9}10void addWord(String w) { for (int i = 0; i <= w.length(); i++) insert(w.substring(i) + "{" + w); }11boolean hasPrefixSuffix(String pre, String suf) { return startsWith(suf + "{" + pre); }
Live state
pattern
"b.d"
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | pattern="b.d" | Search "b.d". A letter follows exactly one edge; a dot tries every child in turn, so the search becomes a DFS on the trie. |
| 2 | L5 | i=1 char=b | The letter 'b' leaves one way: follow it. |
| 3 | L6 | i=1 char=. | A dot at position 1: try each of the 1 child in alphabetical order. |
| 4 | L7 | i=2 branch=a | Try the branch 'a'. |
| 5 | L5 | i=3 char=d | The letter 'd' leaves one way: follow it. |
| 6 | L3 | i=3 end=true | The pattern is used up at a node where a word ends: match. |
| 7 | L7 | return=true nodes visited=4 | search("b.d") = true after 4 node visits. It stops at the first match. |
Practice ladder
Now try it yourself
Easy
- Longest Common Prefix
- Index Pairs of a String
Medium
- Search Suggestions System
- Design Add and Search Words Data Structure
- Map Sum Pairs
Advanced
- Design Search Autocomplete System
- Word Squares
Key takeaways
- Autocomplete = walk to the prefix node, then DFS the subtree in alphabetical order.
- A pass counter on each node turns prefix counting into one O(length of the prefix) lookup.
- Wildcards turn the search into a DFS that branches only at dots.