Tries

Store words letter by letter in a tree so shared prefixes are kept once. Look up and autocomplete in time set by word length, not word count.

14 minLesson 5 of 6
On this page
  1. The idea: one letter per step
  2. Building and searching a trie
  3. The code
  4. Cost
  5. Common mistakes
  6. Check yourself
  7. What to practise next

You type su into a search box and suggestions appear before you finish the word: sun, sum, summer. The app holds a dictionary of thousands of words and answers on every keystroke. Scanning the whole list each time works for a small list and gets slower as the list grows.

Look at the words themselves. Thousands of them start with s. Hundreds start with su. If the words were filed so that everything starting with su sat in one place, a suggestion would be two steps away: go to s, then to u, and read off what is there. That filing system is a trie.

The idea: one letter per step

A trie (usually pronounced "try", from retrieval) is a tree where each edge is a letter. The path from the root to any node spells out a prefix. Words that share a beginning share the same path, so the prefix su is stored once, however many words start with it.

Each node holds two things:

  • children: a map from a letter to the next node, one entry for each letter that can follow this prefix;
  • isWord: a flag that says whether a whole word ends here.

The flag matters because a word can be a prefix of another word. Both so and sort pass through the node for so. Only the flag tells you that so is a word on its own and sor is not.

node for "so" isWord: true children: { r: ● } node for "sor" isWord: false children: { t: ● } "sor" is a path to "sort", but not a word itself
One trie node. It does not store its own letter: the letter is on the edge that leads to it.

Building and searching a trie

To insert a word, start at the root and take one letter at a time. If a child for that letter exists, step into it. If not, create it. After the last letter, set the flag.

To search, walk the same way. If a letter has no child, stop: nothing stored starts with that prefix. If you reach the end of the input, you have the node for that prefix. For "is this a word?", check its flag. For "does any word start with this?", reaching the node is enough. For autocomplete, collect every flagged node below it.

Watch five words go in, then three questions. Each circle shows the letter on the edge that leads into it. Green nodes have the flag set, with the word they complete written below.

Insert so, sort, sun, sum, sky, then searchUse the arrows or ← → keys
insert(word):  node = root  for each letter: go to (or create) that child  mark node as the end of a word find(prefix):  node = root  for each letter: go to that child, or stop if missing  return node
  1. •

    1An empty trie is a single root node, drawn as a dot. It stands for the empty prefix. Every word will hang below it.

  2. •soso

    2Insert so. There is no s child yet, so create it, then create o below it. A word ends at o, so set its flag: green means a word ends here.

  3. •sosortsort

    3Insert sort. s and o already exist, so walk down them without creating anything. Only r and t are new, and the flag goes on t. o stays green: so is still a word.

  4. •sosortsortunsun

    4Insert sun. It shares only the s, so a new branch starts below it.

  5. •sosortsortunsunmsum

    5Insert sum. s and u are shared with sun, so only one node is new.

  6. •sosortsortunsunmsumkysky
    words
    5
    letters
    15
    nodes
    9

    6Insert sky. The five words have 15 letters, but shared prefixes mean the trie needs only 9 letter nodes.

  7. •sosortsortunsunmsumkysky

    7Autocomplete su. Start at the root and follow the s edge.

  8. •sosortsortunsunmsumkysky

    8Then follow u. That is two steps, one per letter, no matter how many words are stored. Everything below this node starts with su, so the other branches can be ignored.

  9. •sosortsortunsunmsumkysky
    suggestions
    sun, sum

    9Collect every green node below u: sun and sum. Those are the suggestions.

  10. •sosortsortunsunmsumkysky
    has(sor)
    false
    startsWith(sor)
    true

    10Is sor a word? Follow s, o, r. The path exists, but r is not green, so no word ends there. The answer is no, even though sor is the start of a word.

  11. •sosortsortunsunmsumkysky
    has(sea)
    false

    11Is sea a word? After s there is no e child, so the search stops at the second letter. A missing word is often rejected before its last letter.

1 / 11

The code

A map of children works for any alphabet, including capital letters, digits and accented characters.

class TrieNode {
  constructor() {
    this.children = new Map(); // letter -> TrieNode
    this.isWord = false;       // does a word end here?
  }
}
 
class Trie {
  constructor() { this.root = new TrieNode(); }
 
  insert(word) {
    let node = this.root;
    for (const ch of word) {
      if (!node.children.has(ch)) node.children.set(ch, new TrieNode());
      node = node.children.get(ch);
    }
    node.isWord = true; // mark the end of the word
  }
 
  // Follow prefix letter by letter. null if the path breaks.
  find(prefix) {
    let node = this.root;
    for (const ch of prefix) {
      node = node.children.get(ch);
      if (node === undefined) return null;
    }
    return node;
  }
 
  has(word) {
    const node = this.find(word);
    return node !== null && node.isWord;
  }
 
  startsWith(prefix) { return this.find(prefix) !== null; }
}
class TrieNode:
    def __init__(self):
        self.children = {}    # letter -> TrieNode
        self.is_word = False  # does a word end here?
 
class Trie:
    def __init__(self):
        self.root = TrieNode()
 
    def insert(self, word):
        node = self.root
        for ch in word:
            if ch not in node.children:
                node.children[ch] = TrieNode()
            node = node.children[ch]
        node.is_word = True  # mark the end of the word
 
    # Follow prefix letter by letter. None if the path breaks.
    def find(self, prefix):
        node = self.root
        for ch in prefix:
            node = node.children.get(ch)
            if node is None:
                return None
        return node
 
    def has(self, word):
        node = self.find(word)
        return node is not None and node.is_word
 
    def starts_with(self, prefix):
        return self.find(prefix) is not None

Autocomplete is find followed by a preorder traversal of the subtree, building each word as it goes down:

// Every stored word that begins with prefix.
function complete(trie, prefix) {
  const out = [];
  const start = trie.find(prefix);
  if (start === null) return out;
  const walk = (node, word) => {
    if (node.isWord) out.push(word);
    for (const [ch, child] of node.children) walk(child, word + ch);
  };
  walk(start, prefix);
  return out;
}
 
const trie = new Trie();
for (const w of ["so", "sort", "sun", "sum", "sky"]) trie.insert(w);
complete(trie, "su"); // ["sun", "sum"]
trie.has("sor");      // false
trie.startsWith("sor"); // true
# Every stored word that begins with prefix.
def complete(trie, prefix):
    out = []
    start = trie.find(prefix)
    if start is None:
        return out
 
    def walk(node, word):
        if node.is_word:
            out.append(word)
        for ch, child in node.children.items():
            walk(child, word + ch)
 
    walk(start, prefix)
    return out
 
trie = Trie()
for w in ["so", "sort", "sun", "sum", "sky"]:
    trie.insert(w)
complete(trie, "su")      # ['sun', 'sum']
trie.has("sor")           # False
trie.starts_with("sor")   # True

Cost

Every operation walks one node per letter of its input. If the word has length L, insert, has and startsWith each take O(L) steps, whether the trie holds 5 words or 5 million. Autocomplete adds the size of the subtree it reads, which is the price of listing the answers.

Memory is at most one node per letter inserted, and less when words share prefixes. Each node carries a map, though, so a trie usually uses more memory than a plain list of the same words.

ApproachTimeSpaceWhy
Hash set of wordsO(L) lookupO(total letters)Great for "is this a word?". A prefix question has to check every word: O(n · L).
TrieO(L) per operationO(total letters)Insert, lookup and prefix check walk one node per letter. Autocomplete adds the size of the answer.

If you only ever ask "is this exact word stored?", a hash set from Hash Tables is simpler and smaller. Reach for a trie when the questions are about prefixes. A sorted array with binary search can also find the range of words with a given prefix, which is a good choice when the word list never changes.

Common mistakes

Check yourself

Quick check

The trie holds so, sort, sun, sum and sky. How many new nodes does inserting sold create?

In the same trie, what does has("su") return?

A trie holds 1 million words. How many steps does has("summer") take, roughly?

What to practise next

Tries, heaps and search trees all lean on one thing: a short tree. Balanced Trees shows how search trees keep themselves short, whatever order the data arrives in.