Tries (Prefix Trees)

March 31, 2026
#trie #prefix-tree #data-structures #autocomplete #string #interviewing

Introduction

A Trie (pronounced “try”) is a tree-like data structure used to store and search strings efficiently. Each node represents a single character, and paths from root to node spell out prefixes. It’s the data structure behind autocomplete, spell checkers, and IP routing tables.

The name comes from “retrieval” — tries are optimized for looking up strings by prefix. Unlike a hash table which gives you O(1) lookup for exact keys, a trie gives you O(m) lookup (where m is the key length) plus the ability to find all keys that share a prefix.

You should be familiar with Binary Trees and Big-O Notation before reading this.

How a Trie works

Insert: "cat", "car", "card", "dog", "do"

        (root)
       /      \
      c        d
      |        |
      a        o *
      |        |
      t *      g *
      |
      r *
      |
      d *

* = end of word

Each path from root to a * node spells a complete word. The prefix “ca” is shared by “cat”, “car”, and “card”.

Implementation

Autocomplete with a Trie

The killer feature — find all words that start with a given prefix:

class Trie {
  // ... previous methods

  autocomplete(prefix) {
    const node = this._findNode(prefix);
    if (!node) return [];

    const results = [];
    this._collect(node, prefix, results);
    return results;
  }

  _collect(node, prefix, results) {
    if (node.isEnd) results.push(prefix);

    for (const [char, child] of Object.entries(node.children)) {
      this._collect(child, prefix + char, results);
    }
  }
}

const trie = new Trie();
['cat', 'car', 'card', 'care', 'cargo', 'dog'].forEach(w => trie.insert(w));

console.log(trie.autocomplete('car'));  // ['car', 'card', 'care', 'cargo']
console.log(trie.autocomplete('do'));   // ['dog']
console.log(trie.autocomplete('z'));    // []

Delete from a Trie

class Trie {
  // ... previous methods

  delete(word) {
    this._deleteHelper(this.root, word, 0);
  }

  _deleteHelper(node, word, depth) {
    if (!node) return false;

    if (depth === word.length) {
      if (!node.isEnd) return false;
      node.isEnd = false;
      return Object.keys(node.children).length === 0;
    }

    const char = word[depth];
    const shouldDelete = this._deleteHelper(node.children[char], word, depth + 1);

    if (shouldDelete) {
      delete node.children[char];
      return !node.isEnd && Object.keys(node.children).length === 0;
    }
    return false;
  }
}

Delete is trickier — you need to remove nodes bottom-up, but only if they’re not part of another word’s path.

Complexity analysis

Operation Time Space
Insert O(m) O(m) per word
Search O(m) O(1)
Prefix search O(m) O(1)
Autocomplete O(m + k) O(k)
Delete O(m) O(1)

Where m = length of the string, k = number of results.

Total space: O(n × m) in the worst case where n is the number of words and m is the average length. In practice, shared prefixes reduce this significantly.

Trie vs Hash Table

Trie Hash Table
Exact lookup O(m) O(m) average
Prefix search O(m) ✅ Not supported ❌
Ordered iteration Yes (alphabetical) No
Space Higher (node overhead) Lower
Autocomplete Natural Requires scanning all keys

Use a trie when you need prefix operations. Use a hash table when you only need exact key lookup.

Practice problems

  1. Implement Trie (LeetCode 208)
  2. Word Search II (LeetCode 212)
  3. Design Add and Search Words (LeetCode 211)
  4. Replace Words (LeetCode 648)
  5. Longest Word in Dictionary (LeetCode 720)
Thanks for visiting
We are actively updating content to this site. Thanks for visiting! Please bookmark this page and visit again soon.
Sponsor