L20. Trie Data Structure & Implementation
A Trie (pronounced "try", derived from "retrieval"), also known as a Prefix Tree or Digital Tree, is an efficient, tree-like data structure used to store and retrieve a dynamic set of strings. Unlike binary search trees or hash tables, keys in a Trie are typically strings, and a node's position within the tree defines its associated key. Tries provide optimal time complexity for prefix-based operations, making them the industry standard for search auto-completion, spell checking, and network routing.
1. Why Do We Need a Trie? (The Problem Statement)
To understand why Tries exist, consider searching for words with a common prefix in standard data structures:
- Array / List of Strings: Finding all words starting with prefix
"ca"requires scanning every string in the list.- Time Complexity: , where is the number of words and is the average string length.
- Hash Map / Hash Set: Direct lookups for an exact match are fast (), but hash tables cannot perform prefix matching efficiently because hash functions destroy the ordering and prefix relationships between strings.
- Trie (Prefix Tree): Locates all words matching a prefix directly by traversing character-by-character along common path branches.
- Time Complexity: for search, insertion, and prefix verification, completely independent of the total number of stored words .
Real-World Applications
- Search Engine Autocomplete: As you type
"CA"into Google, a Trie locates the"CA"node and instantly traverses descendant paths to suggest"cat","car", and"cart". - Spell Checkers: Validates whether a typed string exists along a valid path and suggests corrections by exploring neighboring character nodes.
- IP Routing (Longest Prefix Matching): Routers store destination subnets in binary Tries to match incoming packet IP addresses to target interfaces in constant time.
2. Core Mental Model & Structural Mechanics
Real-Life Analogy
Imagine walking through a multi-story physical dictionary where each floor is designated by a letter ('a' through 'z'). To find the word "car", you enter the building at the Root, take the elevator to the 'c' section, follow a hallway to the 'a' door, and finally enter the 'r' room. The final room contains a physical mark confirming that "car" is a complete word. If you continue walking into the adjacent 't' room, you find another mark for "cart".
Structural Properties
- Root Node: Represents an empty string
""and acts as the entry point for all operations. - Nodes: Store references (pointers) to child character nodes.
- Edges: Represent individual characters connecting parent nodes to child nodes.
- Terminal Flag (
isEnd): A boolean indicator marking whether the path from the root to the current node constitutes a complete, valid word.
Vertical Flow Architecture
Below is the vertical layout of a Trie populated with the words: "cat", "car", "cart", "do", and "dog".
3. Trie Node Architecture & Representation
For standard lowercase English alphabets ('a' through 'z'), each node maintains an array of size 26 containing references to its potential child nodes.
Node Data Structure
class TrieNode {
// Array storing references to child TrieNodes (size 26 for 'a'-'z')
TrieNode[] children;
// Flag indicating if the path ending at this node forms a full word
boolean isEnd;
// Optional field: Stores the complete word string at terminal nodes
// (Used to simplify DFS auto-complete / suggestion retrieval)
String word;
public TrieNode() {
this.children = new TrieNode[26];
this.isEnd = false;
this.word = "";
}
}
Index Calculation Formula
To convert any lowercase ASCII character into its corresponding array index:
Conceptually: Subtracting the ASCII value of
'a'(97) re-indexes character'a'to0,'b'to1, and'z'to25.
4. Operation 1: Word Insertion (insert)
Insertion builds the path for a word character-by-character. If a character node already exists, the algorithm reuses it; if not, it allocates a new TrieNode.
Step-by-Step Logic
- Initialize an iteration pointer
curratroot. - Iterate through each character
chin the target word. - Compute
index = ch - 'a'. - Check if
curr.children[index]isnull:- If
null, create a newTrieNodeatcurr.children[index].
- If
- Move
currpointer tocurr.children[index]. - Once the character loop completes, set
curr.isEnd = true.
Java Implementation
public void insert(String word) {
TrieNode curr = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
// Allocate a new node if the path does not exist
if (curr.children[index] == null) {
curr.children[index] = new TrieNode();
}
// Advance pointer to child node
curr = curr.children[index];
}
// Mark the final node as a valid complete word
curr.isEnd = true;
}
5. Operation 2: Full Word Search (search)
Searching verifies whether a given word was previously inserted into the Trie as a complete word.
Step-by-Step Logic
- Start at
curr = root. - Traverse through each character
chin the inputword. - Compute
index = ch - 'a'. - If
curr.children[index]isnull, returnfalse(the path breaks, word does not exist). - Otherwise, advance
currtocurr.children[index]. - After processing all characters, return
curr.isEnd.
Key Distinction: Returning
truerequires both character path completion ANDcurr.isEnd == true. If the character path exists butisEndisfalse, the search string is merely a prefix, not a full word.
Dry-Run Example
- Search
"car"in Trie containing"cart":'c'exists'a'exists'r'exists.- Node
'r'hasisEnd == true. Returntrue.
- Search
"ca"in Trie containing"cart":'c'exists'a'exists.- Node
'a'hasisEnd == false. Returnfalse.
Java Implementation
public boolean search(String word) {
TrieNode curr = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (curr.children[index] == null) {
return false; // Character branch missing
}
curr = curr.children[index];
}
// Return true only if it is marked as a full word
return curr.isEnd;
}
6. Operation 3: Prefix Search (startsWith)
startsWith checks if any word in the Trie begins with the given prefix.
Step-by-Step Logic
- Start at
curr = root. - Traverse each character
chof the prefix. - Compute
index = ch - 'a'. - If
curr.children[index]isnull, returnfalse. - Advance
currtocurr.children[index]. - If all characters in the prefix are traversed successfully, return
trueimmediately (ignoringisEnd).
Comparison: search vs startsWith
Java Implementation
public boolean startsWith(String prefix) {
TrieNode curr = root;
for (char ch : prefix.toCharArray()) {
int index = ch - 'a';
if (curr.children[index] == null) {
return false; // Prefix broken
}
curr = curr.children[index];
}
return true; // Full prefix path found
}
7. Advanced Operation 4: Autocomplete / Suggestions using DFS
A core production use-case of Tries is generating auto-complete suggestions given a user-entered prefix string.
Architecture Optimization
To streamline suggestion collection during traversal, store the target string directly inside the terminal node during insertion:
// Inside insert() method, at terminal node:
curr.isEnd = true;
curr.word = word; // Storing complete word avoids dynamic string builder overhead
Logic & Workflow
- Navigate from
rootdown the path of the givenprefix. - If the path terminates prematurely (
null), return an empty list (no suggestions). - Once positioned at the end node of the prefix string, execute a Depth-First Search (DFS) from that node down all subtrees.
- During DFS, whenever a node with
isEnd == trueis encountered, append itsnode.wordto the result list.
Vertical Traversal Diagram for Prefix "ca"
Complete Java Implementation (Trie with Autocomplete)
import java.util.*;
public class Trie {
// Internal node structure
static class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean isEnd = false;
String word = "";
}
private final TrieNode root;
public Trie() {
root = new TrieNode();
}
// Insert a word into the Trie
public void insert(String word) {
TrieNode curr = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (curr.children[index] == null) {
curr.children[index] = new TrieNode();
}
curr = curr.children[index];
}
curr.isEnd = true;
curr.word = word; // Store full word at terminal node
}
// Search full word
public boolean search(String word) {
TrieNode curr = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (curr.children[index] == null) return false;
curr = curr.children[index];
}
return curr.isEnd;
}
// Search prefix
public boolean startsWith(String prefix) {
TrieNode curr = root;
for (char ch : prefix.toCharArray()) {
int index = ch - 'a';
if (curr.children[index] == null) return false;
curr = curr.children[index];
}
return true;
}
// Get suggestions for a prefix
public List<String> getSuggestions(String prefix) {
TrieNode curr = root;
List<String> result = new ArrayList<>();
// Step 1: Navigate to the end node of the prefix
for (char ch : prefix.toCharArray()) {
int index = ch - 'a';
if (curr.children[index] == null) {
return result; // Return empty list if prefix does not exist
}
curr = curr.children[index];
}
// Step 2: Perform DFS from the prefix endpoint
dfs(curr, result);
return result;
}
// DFS Helper to explore all subtree branches
private void dfs(TrieNode node, List<String> result) {
if (node == null) return;
// If terminal node reached, collect stored word
if (node.isEnd) {
result.add(node.word);
}
// Explore all 26 possible branches
for (int i = 0; i < 26; i++) {
if (node.children[i] != null) {
dfs(node.children[i], result);
}
}
}
}
8. Time and Space Complexity Analysis
Complexity Matrix
Detailed Complexity Breakdown
- Insertion Time Complexity — :
- Iterates times (once per character). Each array access and node creation takes time.
- Space Complexity — :
- Where is the total number of inserted words and is the average length of a word.
- In the absolute worst-case scenario (where words share no common prefix), total nodes allocated will be .
- In practice, space consumption is significantly lower due to shared common prefix nodes.
- Auto-Complete Suggestions Time — :
- Navigating to the prefix node takes .
- DFS traversal visits matching word nodes, taking time to collect all matches.
9. Real-World Practical Pitfalls & Edge Cases
1. Memory Overhead of Fixed Array Size (26 Pointers)
- Pitfall: Allocating
new TrieNode[26]for every single character node creates manynullreferences in memory, especially when the tree is sparse. - Solution: For large or dynamic alphabets (e.g., Unicode, case-sensitive ASCII), replace the array representation with a hash map:
JAVA
class DynamicTrieNode { Map<Character, DynamicTrieNode> children = new HashMap<>(); boolean isEnd = false; }
2. Case Sensitivity & Character Range Limitations
- Pitfall: Standard
ch - 'a'indexing throwsArrayIndexOutOfBoundsExceptionif non-lowercase characters (e.g., spaces, capital letters, digits) are inserted. - Solution: Normalize inputs via
word.toLowerCase()before processing, or dynamically adjust array boundaries.
3. Infinite Recursion / Null Pointer in DFS
- Pitfall: Calling DFS without verifying whether sub-branches are
nullcauses explicitNullPointerExceptionruntime crashes. - Solution: Always check
node.children[i] != nullbefore recursively invoking DFS.
10. Summary and Key Takeaways
- Prefix Optimization: A Trie (Prefix Tree) is structured specifically to enable time prefix matching, outperforming linear array searches () and hash map limitations.
- Character Paths: Nodes do not explicitly need to store their own character; their character identity is determined by their array index position in the parent's
childrenarray (index = ch - 'a'). - Terminal Marker (
isEnd): Differentiates between valid complete words and intermediate path prefixes. - Shared Prefixes: Words with identical prefixes (e.g.,
"cat","car","cart") reuse the exact same prefix nodes ("ca"), providing high storage efficiency for related text datasets. - Search vs StartsWith:
search(word)demands both a complete path andisEnd == true, whereasstartsWith(prefix)only demands path existence. - Autocomplete via DFS: Suggestions are generated by navigating to the terminal prefix node and executing a Depth-First Search to collect all downstream terminal words.
- Terminal Word Caching: Storing the full
wordstring directly inside terminal nodes eliminates string concatenation overhead during DFS traversal. - Array vs Map Tradeoff: A fixed 26-element array provides optimal lookup speed for fixed lower-case English keys, whereas a
HashMap<Character, TrieNode>provides flexible memory footprint for generalized character sets.