Patricia Trie (Radix Tree / PATRICIA)

What is Patricia Trie

PATRICIA — Practical Algorithm To Retrieve Information Coded In Alphanumeric.
Proposed by Donald R. Morrison in 1968.

Patricia Trie = a compressed Trie where chains of nodes with a single child are compressed into a single edge with a label (a string instead of a single character). This solves the primary problem of a standard Trie — redundant nodes.


Key Idea: Path Compression

Standard Trie for "interview", "internal", "intro":

root → i → n → t → e → r → v → i → e → w ✓
                          ↘ n → a → l ✓
               ↘ r → o ✓

Patricia Trie:

root → "int" → "er" → "view" ✓
                    ↘ "nal"  ✓
            ↘ "ro"  ✓

Visualization

Words: cat, car, card, care, bat, bad

graph TD Root(("∅")) Root -->|"ca"| CA(("ca")) Root -->|"ba"| BA(("ba")) CA -->|"t"| CAT(("t ✓\n'cat'")) CA -->|"r"| CAR(("r ✓\n'car'")) BA -->|"t"| BAT(("t ✓\n'bat'")) BA -->|"d"| BAD(("d ✓\n'bad'")) CAR -->|"d"| CARD(("d ✓\n'card'")) CAR -->|"e"| CARE(("e ✓\n'care'"))

Each edge now carries a string instead of a single character.


Node Structure

┌──────────────────────────────────────┐
│  children: map<string → Node>        │  ← edge key — string (label)
│  is_end: bool                        │
│  value: any                          │
└──────────────────────────────────────┘

Basic Operations

➕ Insertion — O(m)

Three situations are possible during insertion:

Case 1: Label matches exactly

Edge "car" exists, insert "car" → just set is_end = true

Case 2: New word is a prefix of the label

Edge "card" exists, insert "car":
  Split "card" → "car" (is_end=true) + "d" (is_end=true, child)

Case 3: Need to split the label

Edge "card" exists, insert "care":
  Common prefix: "car"
  Split → "car" → "d" ✓
                 ↘ "e" ✓
def insert(node, word):
    for label, child in node.children.items():
        common = longest_common_prefix(label, word)
        if not common:
            continue
        if common == label:                  # case 1/2
            insert(child, word[len(common):])
        else:                                # case 3 — split
            split_node = Node()
            split_node.children[label[len(common):]] = child
            node.children[common] = split_node
            node.children.pop(label)
            if word == common:
                split_node.is_end = True
            else:
                split_node.children[word[len(common):]] = Node(is_end=True)
        return
    node.children[word] = Node(is_end=True)  # new branch

🔍 Search — O(m)

Searching for "care" in Patricia Trie:
  root: check child labels
  Matches "ca" → proceed, remainder "re"
  Matches "r"  → proceed, remainder "e"
  Matches "e"  → proceed, remainder ""
  is_end = true  → FOUND ✓

➖ Deletion — O(m)

After deleting a node — merge the parent with the single remaining child:

Delete "card", only "care" remains:
  Node "car" → {"d": ✓, "e": ✓}
  After deleting "d":
  Node "car" → {"e": ✓}  →  collapse into "care" ✓

Trie vs Patricia Trie — Comparison

{
  "title": { "text": "Node Count: Trie vs Patricia", "left": "center" },
  "tooltip": { "trigger": "axis" },
  "legend": { "data": ["Trie", "Patricia Trie"], "bottom": 0 },
  "xAxis": {
    "type": "category",
    "name": "Number of Words",
    "data": ["10", "50", "100", "500", "1000"]
  },
  "yAxis": {
    "type": "value",
    "name": "Nodes (approximate)"
  },
  "series": [
    {
      "name": "Trie",
      "type": "line",
      "data": [60, 310, 650, 3400, 7000],
      "smooth": true,
      "lineStyle": { "color": "#d9534f", "width": 3 }
    },
    {
      "name": "Patricia Trie",
      "type": "line",
      "data": [19, 99, 199, 999, 1999],
      "smooth": true,
      "lineStyle": { "color": "#5cb85c", "width": 3 }
    }
  ]
}
Trie Patricia Trie
Nodes per word ~word length ~2 (max)
Edge carries 1 character string
Memory ❌ many empty nodes ✅ compact
Search speed O(m) O(m)
Node splitting No Yes (during insertion)
Implementation Simpler More complex
Prefix scan

Variants and Applications

Radix Tree (Radix Trie)

A generalization of Patricia Trie — compression by bit groups (radix 2, 4, 8...).
Used in the Linux kernel for process address space storage.

Where Patricia / Radix Tree is Used

Area Example
🌐 IP Routing Longest Prefix Match in routers
🔤 Autocomplete IDEs, search bars
🗄️ Databases Indexing string fields
🐧 Linux Kernel radix_tree — page cache, IDR
🦀 Rust / Go std BTreeMap uses similar ideas internally
🔑 DNS Domain name lookups

Final Comparison of All Three Structures

B-Tree Trie Patricia Trie
Keys any (ord.) strings strings
Search O(log n) O(m) O(m)
Memory moderate large compact
Prefix Search
Disk-friendly ✅ best
Balancing automatic not needed not needed
Application DBMS, FS dictionaries, NLP routing, OS kernel

BST vs Binary Heap

Main Question

Both structures are binary trees with ordered elements. But they solve different problems and have fundamentally different invariants.


Invariant — Key Difference

graph TD subgraph BST["BST — Horizontal Invariant"] B50(("50")) B30(("30")) B70(("70")) B20(("20")) B40(("40")) B60(("60")) B80(("80")) B50 --> B30 B50 --> B70 B30 --> B20 B30 --> B40 B70 --> B60 B70 --> B80 end subgraph HEAP["Max-Heap — Vertical Invariant"] H80(("80")) H70(("70")) H60(("60")) H50(("50")) H40(("40")) H30(("30")) H20(("20")) H80 --> H70 H80 --> H60 H70 --> H50 H70 --> H40 H60 --> H30 H60 --> H20 end
BST Binary Heap
Invariant left < node < right parent ≥ children (Max)
Order Direction horizontal (left/right) vertical (up/down)
Between Siblings strict order no defined order
Structure arbitrary height always a complete tree

Same Array — Two Different Trees

Data: [20, 30, 40, 50, 60, 70, 80]
graph LR subgraph bst2["BST (ordered insertion — degeneracy!)"] S20(("20")) S30(("30")) S40(("40")) S50(("50")) S60(("60")) S70(("70")) S80(("80")) S20 -->|right| S30 S30 -->|right| S40 S40 -->|right| S50 S50 -->|right| S60 S60 -->|right| S70 S70 -->|right| S80 end
graph TD subgraph heap2["Max-Heap (always balanced)"] H80(("80")) H70(("70")) H60(("60")) H50(("50")) H40(("40")) H30(("30")) H20(("20")) H80 --> H70 H80 --> H60 H70 --> H50 H70 --> H40 H60 --> H30 H60 --> H20 end

BST degenerates into a list — O(n) for all operations.
Heap always remains a complete tree — guaranteed O(log n).


Detailed Operation Comparison

🔍 Arbitrary Search

BST — Searching for 40:
  50 → 30 → 40 ✓   (3 steps = O(log n))

  Each step discards half the tree —
  left or right subtree

Heap — Searching for 40:
  80 → check 70 and 60
     → check 50, 40... ✓  but could have gone right as well

  No horizontal order info —
  must traverse entire tree in worst case

Conclusion: BST wins — O(log n) vs O(n)


🏆 Get Max / Min

Max-Heap — Get Max:
  return heap[0]   ← root, O(1) ✓

BST — Get Max:
  traverse right children to the end
  O(log n) for balanced
  O(n) for degenerate

Min-Heap — Get Min:
  return heap[0]   ← O(1) ✓

BST — Get Min:
  traverse left children to the end
  O(log n) / O(n)

Conclusion: Heap wins — O(1) vs O(log n)


➕ Insertion

BST — Insert 45:
  50 → 30 → 40 → right child empty → insert
  But! May require rebalancing (AVL/RB)

Heap — Insert 45:
  Add to end of array → sift up
  Guaranteed O(log n), no complex rebalancing
BST (simple) BST (AVL/RB) Heap
Insertion O(log n)* O(log n) O(log n)
Guarantee
Implementation complexity low high low

➖ Deletion

Heap — Arbitrary Deletion:
  1. Must FIND the element → O(n)
  2. Only then delete → O(log n)
  Total: O(n) — inefficient!

  Root Removal (min/max) — O(log n) ✓

BST — Arbitrary Deletion:
  1. Find → O(log n)
  2. Delete (3 cases) → O(log n)
  Total: O(log n) ✓

Conclusion: BST wins for arbitrary deletion


📊 Memory Storage

Heap → Array (ideal):

  Index:  0   1   2   3   4   5   6
  Data:  [80, 70, 60, 50, 40, 30, 20]

  parent(i) = (i-1) / 2
  left(i)   = 2*i + 1
  right(i)  = 2*i + 2

  ✅ No pointers
  ✅ Cache-friendly
  ✅ Minimal memory

BST → Nodes with pointers:

  Node { key, left*, right*, parent* }

  ❌ Each node = separate allocation
  ❌ Pointers = extra memory
  ❌ Cache misses during traversal

Complexity Visual Comparison

{
  "title": { "text": "Operation Comparison: BST vs Binary Heap", "left": "center" },
  "tooltip": { "trigger": "axis" },
  "legend": { "data": ["BST (balanced)", "Binary Heap"], "bottom": 0 },
  "xAxis": {
    "type": "category",
    "data": ["Arbitrary Search", "Min/Max", "Insertion", "Delete Min/Max", "Arbitrary Deletion", "Construction"]
  },
  "yAxis": {
    "type": "value",
    "name": "Relative Complexity (n=1000)",
    "max": 1100
  },
  "series": [
    {
      "name": "BST (balanced)",
      "type": "bar",
      "data": [10, 10, 10, 10, 10, 10000],
      "itemStyle": { "color": "#5b9bd5" },
      "label": {
        "show": true,
        "position": "top",
        "formatter": ["O(log n)", "O(log n)", "O(log n)", "O(log n)", "O(log n)", "O(n log n)"]
      }
    },
    {
      "name": "Binary Heap",
      "type": "bar",
      "data": [1000, 1, 10, 10, 1000, 1000],
      "itemStyle": { "color": "#f0ad4e" },
      "label": {
        "show": true,
        "position": "top",
        "formatter": ["O(n)", "O(1)", "O(log n)", "O(log n)", "O(n)", "O(n)"]
      }
    }
  ]
}

Final Summary Table

Operation BST Binary Heap Winner
Arbitrary Search O(log n) O(n) 🏆 BST
Get Min/Max O(log n) O(1) 🏆 Heap
Insertion O(log n) O(log n) 🤝 Tie
Delete Min/Max O(log n) O(log n) 🤝 Tie
Arbitrary Deletion O(log n) O(n) 🏆 BST
In-Order Traversal O(n) ✅ O(n log n) ❌ 🏆 BST
Construction O(n log n) O(n) 🏆 Heap
Memory more less 🏆 Heap
Cache-friendly 🏆 Heap
O(log n) Guarantee ⚠️ AVL/RB only ✅ always 🏆 Heap
Implementation harder easier 🏆 Heap

When to Choose What

flowchart TD Q1{"Only need\nMin or Max?"} Q1 -->|Yes| Q2{"Need arbitrary\ndeletion as well?"} Q1 -->|No| Q4{"Need search\n/ sorted\ntraversal?"} Q2 -->|No| HEAP["✅ Binary Heap\nPriority Queue"] Q2 -->|Yes| Q3{"Speed is\ncritical?"} Q3 -->|"O(log n) for all"| BST2["✅ BST (AVL/RB)"] Q3 -->|Code Simplicity| HEAP2["✅ Heap + auxiliary structure"] Q4 -->|Yes| BST["✅ BST (AVL/RB)\nRange queries, sorting"] Q4 -->|No| Q5{"Data sorted\nduring insertion?"} Q5 -->|Yes| HEAP3["✅ Heap\nno risk of degeneracy"] Q5 -->|No| BST3["✅ BST"] style HEAP fill:#5cb85c,color:#fff style HEAP2 fill:#5cb85c,color:#fff style HEAP3 fill:#5cb85c,color:#fff style BST fill:#5b9bd5,color:#fff style BST2 fill:#5b9bd5,color:#fff style BST3 fill:#5b9bd5,color:#fff

Typical Scenarios

Task Choice Reason
Priority Queue Heap O(1) min/max, simplicity
Dijkstra's Algorithm Heap constantly needs Min
OS Task Scheduler Heap priority-based, O(1) retrieval
HeapSort Heap O(n log n), in-place
Top-K Elements Heap maintain heap of size K
Dictionary / Map BST key search O(log n)
Range Query (A to B) BST In-order traversal of slice
Sorted Set / Ordered Map BST full order required
Stream Median Two Heaps Min-Heap + Max-Heap

Standard Library Implementations

Language Heap BST
🐍 Python heapq (Min-Heap) sortedcontainers.SortedList
☕ Java PriorityQueue TreeMap / TreeSet (Red-Black)
➕ C++ priority_queue std::map / std::set (Red-Black)
🦀 Rust BinaryHeap BTreeMap / BTreeSet
🐹 Go container/heap — (none in stdlib)
🟨 JS — (none in stdlib) — (none in stdlib)

Summary

┌─────────────────────────────────────────────────────┐
│                                                     │
│  Heap  = Min/Max Specialist                         │
│          "Give me the largest/smallest — fast"      │
│          O(1) for the main operation                │
│                                                     │
│  BST   = Universal Ordered Dictionary               │
│          "Find, insert, delete anything"            │
│          O(log n) for everything, full order        │
│                                                     │
└─────────────────────────────────────────────────────┘