Trees

Trie Fundamentals

A prefix tree where every path from root to a terminal node spells a word, and words sharing a prefix share the same path — "cat" and "car" share the nodes for c→a, splitting only at the third character. Every operation — insert, search, prefix query — costs O(L) where L is the word length, completely independent of how many words the trie holds. This structural grouping of shared prefixes is the reason tries power autocomplete, spell-checkers, and IP routing tables.

Foundations Β· Array Β· Linked List Β· Stack Β· Queue Β· HashMap Β· Binary Tree Β· BST Β· Heap Β· Trie Β· Graph Β· Sorting Β· Binary Search Β· Two Pointers Β· Sliding Window Β· Dynamic Prog.
01
Section One Β· Foundation

What is a Trie?

c a t r ● root c a t βœ“ cat r βœ“ car shared prefix "ca" stored once u p u p βœ“ cup three words, one shared 'c' node β€” prefix tree

A trie (pronounced "try") is a tree-like data structure where each node represents a single character and each path from the root to a terminal node spells a complete word β€” the name comes from "retrieval." The defining insight is that words sharing a common prefix share the same path in the tree: inserting "cat", "car", and "cup" creates only one 'c' node because all three words start with 'c', and only one 'a' node for "cat" and "car" because both share the prefix "ca." This shared-prefix property means a trie with 100,000 English words is far smaller than 100,000 separate strings because most words share common beginnings. Every operation β€” insert, search, startsWith β€” runs in O(L) time where L is the word length, completely independent of the number of words stored, because you simply walk one node per character. Unlike a hash map, which gives O(L) exact lookup but requires scanning every key for prefix queries, a trie navigates directly to the prefix node and the entire subtree below it contains all matching words. Java has no built-in Trie class β€” in interviews and practice you always implement it from scratch using a TrieNode with a children[26] array and a boolean isEnd flag.

Analogy: Think of the autocomplete dropdown on your phone's keyboard. As you type each letter, the system instantly narrows down suggestions β€” typing 's' shows all words starting with 's', typing 'se' narrows to 'se*' words, typing 'sea' narrows further to 'sea', 'seal', 'search', 'seat'. The phone isn't scanning its entire dictionary each time; it's walking down a trie one node per keystroke, and at every node the subtree below already contains exactly the matching words β€” grouped by prefix, ready to display. The trie is the data structure that makes "show me everything starting with X" a single downward walk instead of a full dictionary scan.
02
Section Two Β· Mental Model

How It Thinks

Each character occupies one level of the trie
β–Ά
Word length L determines depth, not vocabulary size β€” a trie holding 1 million words is still only 20 levels deep if the longest word has 20 characters
Nodes are shared across words with a common prefix
β–Ά
Inserting "apple" after "app" costs only 2 new nodes ('l' and 'e'), not 5 β€” the first three nodes already exist from "app"
A terminal flag (isEnd) marks word boundaries, not the absence of children
β–Ά
"app" and "apple" can coexist β€” "app"'s final node has isEnd=true AND has child 'l', so it is simultaneously a complete word and a prefix of another word
Search follows the path character by character
β–Ά
If any character's child pointer is null, the word is absent β€” no backtracking, no hash collisions, no comparisons beyond a single array index lookup per character
Prefix search stops at the prefix's last node
β–Ά
All words in the subtree rooted there share that prefix β€” a single DFS/BFS from that node collects every autocomplete result without touching the rest of the trie
Deletion must check whether a node is shared (has other children or marks another word's end)
β–Ά
Blindly removing nodes breaks other words β€” deletion requires careful bottom-up cleanup: only remove a node if it has no children and is not a terminal for another word
03
Section Three Β· Trie Structure

Trie Structure β€” The isEnd Flag

Every trie node holds two things: a children array (or map) that points to the next characters, and a boolean isEnd flag that marks whether a valid word ends at this node. For lowercase English letters, children is a fixed array of 26 slots β€” children[0] for 'a', children[1] for 'b', through children[25] for 'z' β€” where a null slot means that character doesn't follow the current prefix in any stored word. The isEnd flag is essential because a node's mere existence doesn't mean a word ends there β€” the node for 'p' in "app" exists because "apple" passes through it, but only when isEnd=true at that 'p' node does "app" count as a valid word. The classic mistake is confusing "node exists" with "word exists": without checking isEnd, searching "app" in a trie containing only "apple" would incorrectly return true.

TrieNode structure β€” children array + isEnd flag
INTERIOR NODE (isEnd = false) children[26] a b c d … z children['c'-'a'] isEnd false most slots null β€” only used characters have non-null pointers TERMINAL NODE (isEnd = true) children[26] a … l … z has child 'l' isEnd true word ends here AND has children β€” "app" is a word AND prefix of "apple" children['c' - 'a'] = children[2] β†’ index arithmetic maps character to slot 26 slots per node β€” O(26) = O(1) lookup per character
Inserting "app" then "apple" then "apply" β€” isEnd enables coexistence
a p p l e y ● a p p l e βœ“ apple y βœ“ apply isEnd=true word "app" ends here isEnd=false "appl" is NOT a word shared prefix "app" β€” one path, three words branch from it isEnd=true at 'p' β€” "app" is a valid word AND a prefix of "apple"/"apply" isEnd=false at 'l' β€” "appl" exists as a node but is NOT a valid word
TrieNode Structure
Java
class TrieNode { TrieNode[] children = new TrieNode[26]; // a=0, b=1, ... z=25 boolean isEnd = false; // true if a valid word ends here } // Index mapping: char c β†’ children[c - 'a'] // 'a' - 'a' = 0 β†’ children[0] // 'c' - 'a' = 2 β†’ children[2] // 'z' - 'a' = 25 β†’ children[25] // For non-lowercase-only use cases: // HashMap<Character, TrieNode> children = new HashMap<>(); // Trades O(1) array access for memory efficiency on sparse alphabets
Array vs HashMap for children:
  • The children[26] array gives guaranteed O(1) lookup with zero overhead β€” just index by c - 'a'.
  • The downside is 26 pointers per node even if only 2 are used.
  • A HashMap<Character, TrieNode> uses only as much memory as the actual children, but with HashMap overhead per entry.
  • For interviews, almost always use children[26] for lowercase English β€” it's simpler, faster, and interviewers expect it.
  • Switch to HashMap only when the alphabet is large (Unicode) or mixed (letters + digits + symbols).
04
Section Four Β· Operations

Core Operations

Insert
Walk character by character from the root, creating new nodes wherever the child pointer is null, then mark the final node as a word boundary by setting isEnd = true. Always O(L) where L is the word length.
Pseudocode
function insert(word): node = root for each char c in word: idx = c - 'a' if node.children[idx] == null: node.children[idx] = new TrieNode() // create missing node node = node.children[idx] // advance node.isEnd = true // mark word boundary // O(L) β€” one node per character
Insert β€” adding "search" to a trie containing "see"
BEFORE β€” contains "see" s e e ● s e e βœ“ see AFTER β€” "search" inserted s e e a r c h ● s e e βœ“ see a r c h βœ“ search shared 's' and 'e' β€” reused, not duplicated new nodes for 'a','r','c','h' new nodes created only for characters not yet in the trie β€” insert is O(L)
Insert is always O(L):
  • Never O(n) where n is dictionary size.
  • Each character in the word requires exactly one array index lookup and possibly one node allocation.
  • A word of length 6 always costs exactly 6 steps regardless of whether the trie holds 10 words or 10 million.
Search (exact word)
Walk character by character from the root. If any child pointer is null, the word doesn't exist. If you reach the last character, return node.isEnd β€” not just "node exists."
Pseudocode
function search(word): node = root for each char c in word: idx = c - 'a' if node.children[idx] == null: return false // character not in trie node = node.children[idx] // advance return node.isEnd // true only if a word ends here // O(L)
Search β€” "see" found (isEnd=true) vs "sea" not found (no 'a' child)
search("see") β†’ TRUE s e e a ● s e e isEnd=true βœ“ FOUND a step 1: s βœ“ step 2: e βœ“ step 3: e βœ“ + isEnd=true search("sea") β†’ FALSE s e a ● s e e a isEnd=false βœ— NOT A WORD step 1: s βœ“ step 2: e βœ“ step 3: a exists but isEnd=false Two failure modes: (1) child is null β†’ word absent; (2) path exists but isEnd=false β†’ only a prefix
Two failure modes:
  • (1) A child pointer is null β€” the character doesn't follow this prefix in any word, so the word is absent entirely. (2) You reach the last character but isEnd is false β€” the path exists because it's a prefix of another word, but the word itself was never inserted.
  • Both cases return false. This distinction is the most common trie interview mistake.
startsWith (Prefix Search)
Identical to search, but the return condition changes: return true if the prefix path exists, regardless of isEnd. This is the operation that gives tries their structural advantage over hash maps.
Pseudocode
function startsWith(prefix): node = root for each char c in prefix: idx = c - 'a' if node.children[idx] == null: return false // prefix not in trie node = node.children[idx] // advance return true // path exists β€” ignore isEnd // O(L) β€” L = prefix length
startsWith vs search differ by exactly one line:
  • search returns node.isEnd; startsWith returns true.
  • This is the most common interview follow-up after implementing search.
  • The autocomplete use case takes it further: startsWith finds the prefix node, then a DFS/BFS on the subtree collects all complete words below it β€” O(L) to navigate + O(K) to collect K matching characters.
Complexity Reference
Operation Time Space per insert Notes
Insert O(L) O(L Γ— 26) worst L = word length; new nodes only for new chars
Search O(L) O(1) No allocation; follow existing pointers
startsWith O(L) O(1) Same as search; skip isEnd check
Delete O(L) O(1) See Section 7; requires bottom-up check
Autocomplete O(L + K) O(K) L to reach prefix node; K = output characters
05
Section Five Β· Prefix Power

Prefix Patterns β€” Why Tries Beat Hash Maps

A hash map gives O(1) average exact lookup, but answering "find all words starting with 'sea'" requires scanning every key β€” O(nΒ·L) where n is the number of keys and L is average key length. A trie navigates to the prefix node in O(L) and the entire subtree below already contains every matching word, physically grouped by shared prefix. This structural advantage means prefix queries, autocomplete, spell-checking, and longest-prefix-match (IP routing) are all natural trie operations that hash maps cannot perform efficiently. Real-world applications include search engine autocomplete, phone contact search, DNS resolution, and word games like Boggle and Scrabble where you must verify "does any word start with this prefix?" thousands of times per second. When prefix queries are not needed and memory is tight, a simple HashSet<String> is the better choice β€” 26 pointers per trie node is expensive for pure exact-match lookups.

Prefix query "sea*" β€” HashMap O(n) scan vs Trie O(L) navigation
HASHMAP β€” O(n) full scan "search" βœ— check "sea" βœ“ match "seal" βœ“ match "seat" βœ“ match "see" βœ— check "apple" βœ— check "app" βœ— check scan ALL keys O(n) β€” every key checked TRIE β€” O(L) navigate + O(K) collect s e a l β†’ seal t β†’ seat βˆ… β†’ sea (isEnd) ● s e a subtree = all "sea*" words O(3) to reach 'a' node O(K) to collect matches O(L+K) β€” prefix navigation + subtree harvest
Autocomplete β€” collect all words under a prefix
Navigate to the prefix's last node in O(L), then DFS the subtree to collect all complete words β€” O(K) where K is the total characters in matching words.
Pseudocode
function autocomplete(root, prefix): node = navigateTo(root, prefix) // O(L) β€” walk prefix path if node == null: return [] results = [] dfs(node, prefix, results) // O(K) β€” collect all words in subtree return results function dfs(node, current, results): if node.isEnd: results.add(current) for each (char c, child) in node.children: if child != null: dfs(child, current + c, results)
Autocomplete is O(L + K):
  • where K is the total characters across all matching words β€” not just the count of words.
  • DFS and BFS both work; DFS is simpler to implement recursively.
  • In production systems, a limit parameter stops collection after the first N results to avoid collecting the entire subtree when the prefix is short (like "a" matching tens of thousands of words).
Trie vs Alternatives
Structure Exact Lookup Prefix Query Space Best For
HashMap O(L) avg O(nΒ·L) β€” full scan Low Exact lookup only
Sorted Array O(L log n) O(log n + K) range Medium Static dictionary
Trie O(L) O(L + K) High (26Γ—nodes) Prefix queries, autocomplete
Ternary Search Tree O(L) O(L + K) Medium Memory-efficient trie alternative
06
Section Six Β· Variants

Trie Variants β€” Beyond the Standard Trie

The standard trie with children[26] is the interview baseline β€” know it cold before exploring variants. A compressed trie (Radix/Patricia tree) merges chains of single-child nodes into one edge with a string label, reducing node count dramatically for sparse dictionaries while preserving O(L) operations. A suffix trie stores all suffixes of a string, enabling O(L) substring search β€” used in DNA sequence matching and text editors' "find" feature. A bitwise trie uses children[2] (binary digits) instead of children[26] and stores integers bit by bit from MSB to LSB β€” the key structure for maximum-XOR problems on integer arrays. For interviews, compressed tries and bitwise tries appear as follow-ups; suffix structures appear in hard string problems.

Standard Trie vs Compressed Trie (Radix Tree) β€” same words, fewer nodes
STANDARD TRIE β€” 9 nodes s e e a r c h l ● s e e see a r c h search l seal COMPRESSED TRIE β€” 4 nodes "se" "e" "arch" "al" ● se e see arch search al seal single-child chains collapse into one edge edge labels are strings, not single characters
Bitwise Trie β€” storing integers bit by bit for XOR problems
● MSB first 0 1 1 0 1 1 0 1 1 0 1 3 (011) 1 5 (101) XOR Greedy Strategy: at each bit, try the OPPOSITE bit to maximize XOR result 3 XOR 5 = 011 XOR 101 = 110 = 6 β†’ greedy picks opposite at each level children[2] only β€” O(32) = O(1) per query for 32-bit integers
Trie Variants Reference
Variant Children Per Node Key Use Case Interview Frequency
Standard Trie children[26] Autocomplete, spell-check, word search Very High
Compressed (Radix) children[26] Memory-efficient prefix storage Low (conceptual)
Suffix Trie children[26+] Substring search in O(L) Medium (hard problems)
Bitwise Trie children[2] Max XOR, XOR range queries Medium
Ternary Search Tree 3 children Memory-efficient, cache-friendly Low
Bitwise Trie signal:
  • If a problem mentions XOR + array + "maximum" or "queries", think bitwise trie immediately.
  • The pattern: insert each number bit by bit (MSB first, 31 bits for positive ints), then query by greedily choosing the opposite bit at each level to maximize XOR.
  • This gives O(32) per query β€” effectively O(1). LC 421 (Maximum XOR of Two Numbers) is the canonical example.
07
Section Seven Β· Delete

The Delete Problem β€” Careful Cleanup

Deletion in a trie is trickier than insert because nodes may be shared between multiple words β€” blindly removing nodes along a word's path can destroy other words that pass through the same nodes. Three cases arise: (1) the word doesn't exist β€” do nothing; (2) the word is a prefix of another word β€” just clear isEnd at the terminal node but keep the node alive because it has children; (3) the word's path has nodes not shared by other words β€” safely remove nodes bottom-up, stopping when you hit a node that is either a terminal for another word or has other children. The key check at each node during backtracking: can we delete this node? Only if isEnd is false AND all children are null. In practice, most interview problems don't require deletion β€” but understanding when nodes are shared is essential for LC 211 (Design Add and Search Words) and LC 1268 (Search Autocomplete).

Delete β€” three cases requiring different handling
CASE 1: delete "see" (prefix of "seed") s e e d ● s e e d seed βœ“ isEnd: true β†’ false node stays (has child 'd') clear isEnd only β€” node has children CASE 2: delete "seal" (leaf removal) s e a l ● s e a sea βœ“ stays l remove leaf remove 'l' leaf; stop at 'a' (isEnd=true) CASE 3: delete "app" (shared path) a p p lβ†’e ● a p p lβ†’e apple βœ“ isEnd: true β†’ false node stays (child 'l' exists) clear isEnd β€” keep node for "apple"'s path Case 1: word is prefix of another β†’ clear isEnd only Case 2: unique tail (leaf) β†’ remove nodes bottom-up to shared ancestor Case 3: node has other children β†’ clear isEnd, keep node
Delete Implementation
Java
// Returns true if the PARENT should delete its reference to this node boolean delete(TrieNode node, String word, int depth) { if (node == null) return false; // word not in trie if (depth == word.length()) { if (!node.isEnd) return false; // word not actually stored node.isEnd = false; // unmark word boundary return isEmpty(node); // safe to remove if no children } int idx = word.charAt(depth) - 'a'; if (delete(node.children[idx], word, depth + 1)) { node.children[idx] = null; // child was deleted β€” null out ref return !node.isEnd && isEmpty(node); // can we delete this node too? } return false; // child was kept β€” keep this node } boolean isEmpty(TrieNode node) { for (TrieNode child : node.children) if (child != null) return false; return true; // all 26 children are null } // O(L) time, O(L) stack space
The boolean return value is the key insight: true means "the child was safely deleted β€” parent should null out its reference to it." false means "the child must stay β€” either the word wasn't found, or the child node has other children or is a terminal for another word." This bottom-up signal propagation is the same pattern as BST delete's return-node trick β€” the decision to keep or remove happens on the way back up the recursion stack.
Common Mistake: Just setting isEnd = false without bottom-up cleanup. This is technically correct for "search" behavior β€” the word will no longer be found β€” but it leaks nodes. Over thousands of inserts and deletes, the trie accumulates dead branches that waste memory. In interview settings, clarify whether deletion must reclaim memory or just logically remove the word. If the interviewer says "just mark it", isEnd = false is sufficient. If they want full deletion, you need the recursive approach above.
08
Section Eight Β· Implementation

Build It Once

Build this from scratch once β€” it makes the mechanics concrete. In any real project or interview, start from this skeleton.

Java β€” Trie core mechanics
class TrieNode { TrieNode[] children = new TrieNode[26]; boolean isEnd; } class Trie { TrieNode root = new TrieNode(); // INSERT β€” O(L) void insert(String word) { TrieNode node = root; for (char c : word.toCharArray()) { int i = c - 'a'; if (node.children[i] == null) node.children[i] = new TrieNode(); node = node.children[i]; } node.isEnd = true; } // SEARCH β€” O(L) boolean search(String word) { TrieNode node = find(word); return node != null && node.isEnd; } // STARTS WITH β€” O(L) boolean startsWith(String prefix) { return find(prefix) != null; } private TrieNode find(String s) { TrieNode node = root; for (char c : s.toCharArray()) { node = node.children[c - 'a']; if (node == null) return null; } return node; } }
09
Section Nine Β· Java Reference

Use It In Java

IN JAVA β€” No built-in Trie; build from TrieNode
Standard Interview Trie Pattern
Trie trie = new Trie(); trie.insert("apple"); trie.insert("app"); trie.search("apple"); // β†’ true (exact word exists) trie.search("app"); // β†’ true ("app" was inserted) trie.search("ap"); // β†’ false ("ap" was never inserted) trie.startsWith("app"); // β†’ true (prefix exists β€” "app", "apple") trie.startsWith("xyz"); // β†’ false (no word starts with "xyz") // Key distinction: search("ap") is false, startsWith("ap") is true // search checks isEnd; startsWith checks only path existence
Adapting Trie for Different Inputs
// Lowercase English only β€” most interview problems TrieNode[] children = new TrieNode[26]; // index = ch - 'a' // Case-insensitive β€” toLowerCase() before insert/search word = word.toLowerCase(); // normalize first // Full ASCII (128 chars) β€” letters + digits + symbols TrieNode[] children = new TrieNode[128]; // index = (int) ch // Unicode / arbitrary chars β€” HashMap-based HashMap<Character, TrieNode> children = new HashMap<>(); // Digits only β€” phone numbers, IP addresses TrieNode[] children = new TrieNode[10]; // index = ch - '0'
When to reach for a Trie in interviews
Problem Signal Structure Why
"autocomplete / all words with prefix X" Trie O(L+K) vs O(nΒ·L) hash scan
"word exists in dictionary" (static) HashSet<String> O(L) with less code; trie is overkill
"maximum XOR of two numbers" Bitwise Trie Greedy opposite-bit traversal
"word search on a 2D board" DFS + Trie Trie prunes when checking many words
"design search autocomplete system" Trie + frequency Classic design question
⚠ Gotcha: Always validate ch - 'a' is in [0, 25] before indexing children[]. Non-lowercase input causes ArrayIndexOutOfBoundsException silently if unchecked. Add a guard: if (ch < 'a' || ch > 'z') throw new IllegalArgumentException(), or use a HashMap-based node for mixed input.
⚠ Gotcha: Some interview variants ask insert to return a boolean (was the word already present). The check: read node.isEnd before setting it to true, and return the old value. If it was already true, the word was a duplicate.
10
Section Ten Β· Practice

Problems To Solve

Trie problems cluster into three types: implement the trie itself (LC 208), use the trie to solve a string/prefix problem (LC 212, 421), or design a system using a trie (LC 642, 1268). The hardest trie problems combine trie with DFS β€” Word Search II builds a trie from the dictionary and DFS-es the board simultaneously, pruning branches the moment a prefix has no match. Recognise the trie signal: "prefix", "autocomplete", "words starting with", "maximum XOR." For design problems, the trie is always built incrementally on insert, never rebuilt. Interview strategy: implement TrieNode + insert + search + startsWith first β€” these three methods appear on 90% of trie problems.

Difficulty Pattern Problem Key Insight
Medium trie Implement Trie (Prefix Tree) β€” LC 208 Implement TrieNode with children[26] and isEnd. search returns node.isEnd; startsWith returns node != null. The only difference between the two is the final check.
Hard dfs + trie Word Search II β€” LC 212 Build a trie from the word list; DFS the board while walking the trie simultaneously. When node.isEnd is true, add the word. Store the word at the terminal node instead of reconstructing from the path.
Medium trie + dfs Design Add and Search Words β€” LC 211 Insert is standard. Search handles '.' wildcards by trying all 26 children at that position β€” recursive DFS on the trie, not the input string.
Medium bitwise trie Maximum XOR of Two Numbers β€” LC 421 Insert each number bit by bit (MSB first, 31 bits for int). For each number, query the trie greedily: at each bit, try the opposite bit first to maximize XOR.
Medium trie Replace Words β€” LC 648 Insert all roots into the trie. For each word in the sentence, walk the trie character by character; return the root the moment isEnd is true.
Hard trie + ranking Design Search Autocomplete System β€” LC 642 Build a trie on insert; store (sentence, frequency) at terminal nodes. On each character input, navigate to the current prefix node, then DFS to collect all sentences. Sort by frequency descending.
Interview Pattern:
  • When you see "prefix", reach for a trie. When you see "XOR maximum", reach for a bitwise trie.
  • When you see "word search on a grid with a dictionary", reach for trie + DFS.
  • The trie's power is spatial grouping β€” words sharing a prefix share nodes, which means the data structure physically navigates you to the right answer rather than scanning all options.
  • In every case, the trie acts as a pruning device: it tells you "no word starts with this prefix" in O(L), letting you abandon a search path early.

β†’ See the full Trie practice set