Trie · 5 of 6
Implement a Phone Directory
As someone types a name letter by letter, show every contact that starts with what they typed so far.
- Simple way
- time O(Q * N * L)
room O(N * L) - Clever way
- time O(N * L + Q * S)
room O(26 * N * L)
The problem, simply
You get a list of contacts and a query string that a user types one letter at a time. After each letter, show all contacts that start with the letters typed so far, in ABC order and without repeats. If no contact matches, show 0.
Below, N is the number of contacts, L is the longest contact, Q is the length of the query, and S is the number of letters in the contacts you show.
A story to remember it
Picture it
The trie for the contacts ann, amy, anna and bob. Every name that starts with a hangs below the a circle. Double rings mark where a contact ends.
Examples
A normal case
- In
contacts = [ann, amy, anna, bob], query = 'ane'- Out
[[amy, ann, anna], [ann, anna], [0]]
a matches three names. an matches two. Nothing starts with ane.
Every prefix matches
- In
contacts = [geeks, geek, gear], query = 'gee'- Out
[[gear, geek, geeks], [gear, geek, geeks], [geek, geeks]]
g and ge match all three. gee drops gear.
Repeated contacts
- In
contacts = [bob, bob, bo], query = 'b'- Out
[[bo, bob]]
bob is stored once. A trie removes repeats for free, because the same name ends at the same circle.
No match from the start
- In
contacts = [ann], query = 'xy'- Out
[[0], [0]]
There is no x branch. Once you fall off, every longer prefix is also 0.
Way 1: the simple way
For each prefix, look at every contact and keep the ones that start with it.
- Put the contacts in a
TreeSet. That removes repeats and keeps ABC order. - For each length
1toQ, take the prefix of the query. - Collect every contact that starts with the prefix. If none, add
0.
import java.util.*;
class Solution {
public static List<List<String>> displayContacts(String[] contacts, String query) {
TreeSet<String> book = new TreeSet<>(Arrays.asList(contacts)); // sorted, no repeats
List<List<String>> out = new ArrayList<>();
for (int len = 1; len <= query.length(); len++) {
String prefix = query.substring(0, len);
List<String> hits = new ArrayList<>();
for (String c : book) {
if (c.startsWith(prefix)) hits.add(c);
}
if (hits.isEmpty()) hits.add("0");
out.add(hits);
}
return out;
}
}How long it takes: Q prefixes, each checking N contacts for up to L letters, O(Q * N * L), plus the sort. Extra room: the sorted copy, O(N * L).
Way 2: the clever way
Put every contact in a trie. Each new letter is one step down from where you already are. Everything below that node is a match, so collect it with a depth first walk that tries children in ABC order.
- Insert all contacts into a trie.
- Keep a finger
curat the root. For each typed letter, movecurdown that letter's branch. - If the branch is missing, this prefix and every longer one get
0. - Otherwise walk everything below
cur. Each word end you meet is a suggestion, already in ABC order.
Step down one letter, then read everything below
01 / 08
Put every contact into a trie first: ann, amy, anna and bob. Children are kept in ABC order, so a walk from left to right meets names in ABC order.
- looking now
- done
- answer
- rejected
- path
- just added
import java.util.*;
class Solution {
private static class Node {
Node[] next = new Node[26];
boolean end;
}
public static List<List<String>> displayContacts(String[] contacts, String query) {
Node root = new Node();
for (String c : contacts) { // build the trie
Node cur = root;
for (char ch : c.toCharArray()) {
int k = ch - 'a';
if (cur.next[k] == null) cur.next[k] = new Node();
cur = cur.next[k];
}
cur.end = true; // repeats land on the same node
}
List<List<String>> out = new ArrayList<>();
Node cur = root;
StringBuilder prefix = new StringBuilder();
for (char ch : query.toCharArray()) {
prefix.append(ch);
cur = (cur == null) ? null : cur.next[ch - 'a']; // one step from where we are
List<String> hits = new ArrayList<>();
if (cur != null) collect(cur, prefix, hits);
if (hits.isEmpty()) hits.add("0");
out.add(hits);
}
return out;
}
// depth first walk in ABC order: every word end below node is a match
private static void collect(Node node, StringBuilder word, List<String> hits) {
if (node.end) hits.add(word.toString());
for (int k = 0; k < 26; k++) {
if (node.next[k] != null) {
word.append((char) ('a' + k));
collect(node.next[k], word, hits);
word.deleteCharAt(word.length() - 1);
}
}
}
}How long it takes: building the trie is O(N * L). Each prefix costs one step plus a walk over the part of the trie below it, which is about the size of what we print, so O(N * L + Q * S) in all. Extra room: the trie, O(26 * N * L).
Watch out for
- Repeated contacts. With a list they show up twice. In a trie they end at the same node, so they appear once.
- The
0for no match. Many versions want the string0, not an empty list. Read the problem. - Children in a
HashMaplose ABC order. Use an array of 26, or aTreeMap, if the order matters.
What interviewers ask next
- Show only the top 3 suggestions, like a search box? Store the best 3 words in every node while inserting. Then each keystroke is one step and a copy, no walk at all.
- Rank by how often a name is picked? Keep a count at each word end, and keep the top
kper node in a small heap. - Huge directories that do not fit in memory? Use a compressed trie, or a sorted file plus binary search for the range of words with the prefix.
- Can sorting alone do it? Yes. Sort once, then for each prefix binary search for the first and last word that start with it. Each prefix costs
O(L log N)plus the output.
Remember this
Grown-up words: the drawer cabinet is a trie, the drawer you have open is the current node, reading every card inside is a depth first traversal of its subtree, and the whole feature is autocomplete.