COMPUTER SCIENCE FUNDAMENTALS SERIES

Tries

Prefix trees · Radix trees · Suffix trees ·
Autocomplete · Patricia tries · IP routing
Mid-level software engineer track  ·  20 slides
02

What Is a Trie?

Definition

A trie (from retrieval) is a tree-shaped data structure where each node represents a single character of a key. Keys sharing a common prefix share the same path from the root.

  • The root node is empty (represents the empty string)
  • Each edge is labelled with a character from the alphabet
  • Every path from root to a flagged node spells out a stored key
  • No node stores the key itself — the key is implicit in the path
  • Also called a prefix tree or digital tree

First described by Fredkin (1960) and de la Briandais (1959). Pronounced "try" to distinguish from "tree".

Basic structure

           (root)
          /   |   \
         c    d    t
         |    |   / \
         a    o  h   o
        / \   |  |   |
       r   t  g  e   p
       |
       s
     (cars) (cat) (dog) (the) (top)

Key insight: "car" and "cars" share the path c → a → r. Shared prefixes mean shared storage — this is the trie's fundamental advantage.

03

Node Structure & Alphabet Size

Array-based node (fixed alphabet)

class TrieNode:
    def __init__(self):
        self.children = [None] * 26  # a-z
        self.is_end = False
        self.value = None            # optional

Hash-map-based node (variable alphabet)

class TrieNode:
    def __init__(self):
        self.children = {}           # char -> TrieNode
        self.is_end = False

Comparison

ApproachChild lookupMemory / nodeBest for
ArrayO(1)O(|Σ|)Small, fixed alphabets
Hash mapO(1) amortisedO(children)Unicode, sparse
Sorted listO(log k)O(children)Memory-critical

Key trade-off: alphabet size |Σ| dominates memory. A 26-slot array wastes space in sparse nodes; a hash map adds overhead per entry.

04

Insertion

Algorithm

  1. Start at the root node
  2. For each character c in the key: if children[c] exists, move to that child; otherwise create a new node and move to it
  3. Mark the final node as is_end_of_word = True
def insert(root, word):
    node = root
    for char in word:
        idx = ord(char) - ord('a')
        if node.children[idx] is None:
            node.children[idx] = TrieNode()
        node = node.children[idx]
    node.is_end = True

Complexity: O(m) where m is key length. No rebalancing, no hashing, no comparisons beyond single characters.

Inserting "cat", "car", "card"

After "cat":     After "car":
    (root)           (root)
      |                |
      c                c
      |                |
      a                a
      |               / \
      t*             t*   r*

After "card":
    (root)
      |
      c
      |
      a
     / \
    t*   r*
         |
         d*
05

Search

Exact search

  1. Start at the root
  2. For each character c: if children[c] is None, return False; else move to that child
  3. Return True only if is_end_of_word = True
def search(root, word):
    node = root
    for char in word:
        idx = ord(char) - ord('a')
        if node.children[idx] is None:
            return False
        node = node.children[idx]
    return node.is_end

Prefix check

def starts_with(root, prefix):
    node = root
    for char in prefix:
        idx = ord(char) - ord('a')
        if node.children[idx] is None:
            return False
        node = node.children[idx]
    return True  # path exists = prefix exists

Key distinction: search("car") requires is_end = True. starts_with("car") only requires the path to exist. This difference is the basis for autocomplete.

06

Deletion

Algorithm

  1. Recursively traverse to the end of the key
  2. Unmark is_end_of_word
  3. On the way back up, delete nodes that are no longer end-of-word markers AND have no remaining children
def delete(root, word, depth=0):
    if root is None:
        return False
    if depth == len(word):
        if not root.is_end:
            return False
        root.is_end = False
        return not any(root.children)
    idx = ord(word[depth]) - ord('a')
    should_delete = delete(
        root.children[idx], word, depth + 1)
    if should_delete:
        root.children[idx] = None
        return (not root.is_end
                and not any(root.children))
    return False

Delete "cars" from {car, cars, cat}

Before:          After:
  c                c
  |                |
  a                a
 / \              / \
t*   r*          t*   r*
     |
     s*  <-- removed

Complexity: O(m). Recursive cleanup ensures no orphaned chains waste memory. Nodes shared with other keys ("car") are preserved.

07

Prefix Search & Autocomplete

Collecting all words with a given prefix

  1. Navigate to the node representing the prefix
  2. Run DFS from that node, collecting all paths to is_end nodes
def autocomplete(root, prefix):
    node = root
    for char in prefix:
        idx = ord(char) - ord('a')
        if node.children[idx] is None:
            return []
        node = node.children[idx]
    results = []
    _dfs(node, prefix, results)
    return results

def _dfs(node, current, results):
    if node.is_end:
        results.append(current)
    for i in range(26):
        if node.children[i]:
            _dfs(node.children[i],
                 current + chr(i + ord('a')),
                 results)

Example

Prefix "ca" -> navigate to 'a' under 'c'
DFS finds: "car", "card", "cars", "cat"

Complexity: O(p + k) where p is prefix length and k is total characters in all matches. No other data structure supports this as efficiently.

Ranked autocomplete

Store frequency weights at each end node. Use a priority queue during DFS to return top-k results by popularity.

08

Time & Space Complexity

Time complexity

OperationTrieHash TableBalanced BST
InsertO(m)O(m) avgO(m log n)
SearchO(m)O(m) avgO(m log n)
DeleteO(m)O(m) avgO(m log n)
Prefix searchO(p + k)O(n · m)O(n · m)
Sorted traversalO(N)O(n log n)O(N)

m = key length, n = number of keys, p = prefix length, k = output size, N = total characters

Space complexity

Worst case

O(n · m · |Σ|) — every key branches at every character. Array nodes allocate |Σ| pointers even when mostly NULL.

Best case

Heavily shared prefixes reduce space significantly. Hash-map nodes allocate proportional to actual branching only.

Key insight: tries trade space for deterministic O(m) operations. The guarantee is per-key, independent of stored key count — unlike hash tables where collisions degrade performance.

09

Compressed Tries — Patricia / Radix Trees

The problem with standard tries

Chains of single-child nodes waste memory:

Standard trie for
{"romane", "romanus", "romulus"}:

r -> o -> m -> a -> n -> e*
                       -> u -> s*
               -> u -> l -> u -> s*

Many single-child nodes.

Radix tree (compressed trie)

Merge single-child chains into edges with multi-character labels:

Radix tree:
       "rom"
      /     \
   "an"     "ulus"*
   /   \
  "e"*  "us"*

Patricia trie

Practical Algorithm To Retrieve Information Coded In Alphanumeric (Morrison, 1968). A radix tree where every internal node has at least two children.

PropertyStandardRadix/Patricia
Nodes for n keysO(n · m)O(n)
OperationsO(m)O(m)
Edge labelsSingle charSubstrings

In practice: radix trees are the standard choice for memory-efficient prefix matching. Used in Linux kernel routing tables, Redis cluster key distribution, and HTTP routers.

10

Ternary Search Tries

Structure

Each node has three children:

  • Left: characters less than current
  • Middle: characters equal (continue down the key)
  • Right: characters greater than current
Insert "cat", "cup", "bat":

         c
        /|\
       b  a  u
       |  |  |
       a  t* p*
       |
       t*

Comparison

FeatureStandard TrieTST
Children / node|Σ| array3 pointers
MemoryHigh (sparse)Low
Search timeO(m)O(m + log n)
Prefix searchNativeSupported
Sorted orderIn-orderIn-order

Best for: TSTs combine the time efficiency of tries with the space efficiency of BSTs. Particularly effective for large alphabets (Unicode) where array-based tries waste enormous memory.

11

Suffix Tries

Definition

A suffix trie for string S of length n is a trie containing all n suffixes of S.

Example: "banana$"

Suffixes: banana$, anana$, nana$, ana$, na$, a$, $

Insert all suffixes into a standard trie:

      (root)
    / | | | \ \  \
   b  a  n  $  ...
   |  |  |
   a  n  a
   |  |  |
   n  a  n
  ... ... ...

Properties

  • Every substring of S is a prefix of some suffix — so substrings can be found by prefix search
  • Substring search: O(m) — search the trie for the pattern
  • Count occurrences: count leaves below the matching node
  • Space: O(n2) — each suffix has O(n) characters

Sentinel character: the $ ensures no suffix is a prefix of another, guaranteeing every suffix ends at a leaf. The quadratic space cost motivates suffix trees.

12

Suffix Trees

From suffix trie to suffix tree

Apply path compression (radix tree) to the suffix trie — merge single-child chains into edges labelled with substrings.

Suffix tree for "banana$":

         (root)
       /   |    \
     $   a       banana$
        / \
      $   na
         / \
        $  na$

Key properties

  • Space: O(n) nodes and edges
  • Construction: Ukkonen's algorithm — O(n) online
  • Substring search: O(m) for pattern of length m
  • Longest repeated substring: deepest internal node

Ukkonen's algorithm (1995)

  • Builds suffix tree incrementally, left to right
  • Three extension rules: leaf extension, new branch, do nothing
  • Suffix links for amortised linear time
  • Active point tracks position: (active_node, active_edge, active_length)

Power: suffix trees solve dozens of string problems in optimal time — pattern matching, longest repeated substring, string statistics, and more.

Alternative: suffix arrays

A sorted array of all suffix starting positions. Uses less memory than suffix trees with similar query capabilities. Constructible in O(n) via SA-IS algorithm.

13

Generalised Suffix Trees

Definition

A generalised suffix tree (GST) stores the suffixes of multiple strings simultaneously. Each leaf is labelled with both a string index and suffix position.

Construction

  1. Concatenate strings with unique sentinels: banana$1bandana$2
  2. Build suffix tree on the concatenation
  3. Mark each leaf with its originating string

Construction remains O(N) where N is the total length of all strings.

Applications

ProblemSolution via GST
Longest common substringDeepest internal node with leaves from all strings
Common substrings of k/nNodes with leaves from ≥ k distinct strings
Suffix-prefix overlapUsed in genome assembly overlap graphs
Multiple pattern searchBuild GST of patterns; stream text through it

Bioinformatics: GSTs are foundational in genomics. Assemblers like Velvet and SGA use them to find overlapping reads efficiently.

14

Trie vs Hash Table vs BST

FeatureTrieHash TableBalanced BST
SearchO(m) deterministicO(m) avg, O(nm) worstO(m log n)
InsertO(m) deterministicO(m) avg, O(nm) worstO(m log n)
Prefix queriesNative, O(p + k)Not supportedNot supported
Sorted orderNatural (DFS)Requires sortingNatural (in-order)
Worst-case guaranteeAlways O(m)Degrades with collisionsO(m log n)
MemoryHigh (pointer-heavy)ModerateModerate
Cache localityPoor (pointer chasing)Good (contiguous)Moderate

Use a Trie when

Prefix queries, autocomplete, longest prefix match, sorted iteration needed

Use a Hash Table when

Exact-match lookups only, no prefix queries, memory-sensitive workloads

Use a BST when

Sorted order and range queries needed, no prefix operations required

15

Memory Optimisation Techniques

Array compression

Replace fixed-size child arrays with a bitmask + compact array. Only non-null pointers are stored. Per-node memory drops from O(|Σ|) to O(children).

Alphabet reduction

Map characters to smaller ranges. Case-insensitive a-z = 26 slots instead of 256 for ASCII. Encode Unicode as UTF-8 bytes — alphabet drops to 256.

Path compression (radix tree)

Eliminate single-child chains. Node count drops from O(n · m) to O(n).

LOUDS / succinct encoding

Level-Order Unary Degree Sequence: encode trie topology in ~2 bits per node. Store labels in a flat array. Near-theoretical-minimum space with rank/select for navigation.

Double-array trie

Two parallel arrays: base[] and check[]. Achieves array-trie speed with compressed memory. Used in Japanese morphological analysers (MeCab, Darts).

In practice: a radix tree with hash-map nodes covers most use cases. Succinct tries (LOUDS, DFUDS) are reserved for static dictionaries where minimising memory is paramount.

16

Bitwise Tries

Binary trie

A trie over the alphabet {0, 1}. Keys are bit strings. Each level inspects one bit.

Insert 5 (101), 2 (010), 7 (111):

       (root)
      0/    \1
     (0)    (1)
    0/ \1  0/ \1
   ( ) ( ) (1) ( )
   |   |   |    |
  ... 2*  5*   7*

Crit-bit tree

Only branch at "critical bits" where keys differ — skip identical prefix bits. Minimal branching, fast lookups.

Applications

ApplicationWhy bitwise trie?
IP routing (LPM)Match destination IP against prefixes bit by bit
XOR nearest neighbourKademlia DHT uses XOR distance on bitwise tries
Integer setsPredecessor/successor in O(w) for w-bit keys
Van Emde BoasBitwise trie over integer universe, O(log log U)

Linux kernel: IPv4/IPv6 longest prefix match uses a multi-bit trie (LC-trie) — inspecting several bits per level for speed.

17

Burst Tries

Motivation

Standard tries have poor cache locality — every character lookup follows a pointer to a different memory location.

Structure

  • Internal nodes are standard trie nodes
  • Leaf nodes are containers (sorted arrays, small hash maps) holding key suffixes
  • When a container exceeds a threshold, it bursts into trie nodes
Burst trie:

     (root)
    /      \
   a        b
   |        |
 [pple,    [at, oy,
  rt,       us]*
  xe]*

* = container (sorted list)

Performance

FeatureStandard TrieBurst Trie
Cache missesOne per characterAmortised — fewer dereferences
MemoryHighLower (compact containers)
InsertO(m)O(m) amortised
SearchO(m)O(m) amortised

Origin: Heinz, Zobel & Williams (2002). Burst tries consistently outperform standard tries and hash tables on string-heavy workloads due to superior cache behaviour.

18

Applications — Autocomplete & Spell Checking

Autocomplete

  • Search engines: type "how to" — suggest completions ranked by popularity
  • IDEs: type str. — list methods starting with that prefix
  • Implementation: trie with frequency weights; top-k via priority queue
Weighted trie for autocomplete:

    (root)
      |
      c
      |
      a
     / \
    r    t  (freq: 500)
    |
   (freq: 300)
    |
    s  (freq: 100)

Spell checking

  • Build a trie of dictionary words
  • For a query, search within edit distance d: traverse the trie tracking insertions, deletions, substitutions (Levenshtein)
  • Prune branches where accumulated distance exceeds d
  • Much faster than checking edit distance against every word

Word games

Boggle / Scrabble solvers: DFS on the board guided by trie edges — prune invalid prefixes immediately.

Scale: Google's autocomplete processes billions of queries daily using distributed trie-like structures with frequency-weighted ranking.

19

Applications — IP Routing & T9 Keyboard

IP routing — longest prefix match

Routers store rules as IP prefixes with varying lengths. For each packet, find the longest matching prefix.

Routing table:
  10.0.0.0/8     -> Gateway A
  10.1.0.0/16    -> Gateway B
  10.1.1.0/24    -> Gateway C

Packet to 10.1.1.42:
  Matches all three
  Longest prefix = /24 -> Gateway C
  • Binary trie on IP bits: O(W) lookup (W = 32 for IPv4, 128 for IPv6)
  • Multi-bit tries (LC-trie) inspect 4-8 bits per level
  • Hardware TCAM achieves single-cycle lookup

T9 predictive text

Map phone keypad digits to letters: 2=abc, 3=def, ... Build a trie keyed on digit sequences.

Input: 4-3-5-5-6
Trie path: 4 -> 3 -> 5 -> 5 -> 6
Matches: "hello", "jello"

Other applications

  • Genome indexing: suffix trees index DNA for alignment (Bowtie, BWA)
  • Packet classification: multi-dimensional tries for firewall rules
  • Compiler symbol tables: fast prefix-based identifier lookup

Ubiquity: tries are embedded in systems developers use daily — from the router forwarding their packets to the keyboard predicting their words.

20

Summary & Further Reading

Key takeaways

  • A trie stores keys as paths — prefix sharing is the core advantage
  • Insert, search, delete are O(m) — independent of stored key count
  • Prefix search and autocomplete are native trie operations with no hash table equivalent
  • Compressed tries (radix/Patricia) reduce space to O(n) nodes
  • Suffix trees solve substring problems in linear time
  • Ternary search tries balance memory and speed for large alphabets
  • Bitwise tries power IP routing and integer data structures
  • Burst tries improve cache locality via compact containers
  • Choose tries for prefix ops and worst-case guarantees; hash tables for raw exact-match speed

Recommended reading

SourceDescription
Sedgewick & WayneAlgorithms, 4th Ed. — tries and TSTs
GusfieldAlgorithms on Strings, Trees, and Sequences — suffix trees
Ukkonen (1995)On-line construction of suffix trees
Heinz et al. (2002)Burst Tries — fast string key structure
KnuthTAOCP Vol. 3 — digital searching
Morrison (1968)PATRICIA — the original compressed trie
Prefix tree Radix tree Suffix tree Patricia TST Burst trie LC-trie