Hello! Welcome to the next lesson in our journey through data structures.
In the last session, we saw how hashing with a HashSet provides a powerful, general-purpose way to solve problems requiring fast lookups. Today, we shift our focus to a more specialized data structure designed specifically for strings and prefixes: the Trie, also known as a prefix tree.
Tries are fundamental in solving a wide range of interview problems related to dictionaries, string searching, and autocomplete systems. Your experience with projects like the AI Sentry Gun, which involves real-time processing, highlights the importance of choosing efficient, purpose-built data structures, and the Trie is a prime example of this.
Our goal for this lesson is to implement a trie (prefix tree) with insert and search operations. We will cover its structure, the logic behind its core operations, and implement it from scratch in Java.
1. What is a Trie?
A Trie is a tree-like data structure where each node stores a character. By traversing from the root down a path of nodes, you can spell out a word or a prefix. A key feature is that words with common prefixes share the same initial path in the tree, making it highly memory-efficient for storing large dictionaries.
Imagine you want to store the words "and", "ant", "dad", and "do". A Trie would organize them like this:

2. The Structure of a Trie Node
To build a Trie, we first need to define the structure of its nodes. Since we are focusing on interview and competitive programming contexts, we'll assume words consist of only lowercase English letters ('a' through 'z').
Each node in our Trie needs two key pieces of information:
- An array of children nodes: Since there are 26 possible lowercase letters, each node will have an array of 26 pointers to other
TrieNodes. The pointer at index 0 will correspond to the character 'a', index 1 to 'b', and so on. We can easily find the index for a charactercwith the expressionc - 'a'. - A boolean flag: We need a way to know if a path from the root to the current node represents a complete word that was inserted, or just a prefix. A flag, let's call it
isEndOfWord, serves this purpose.
Here's how we can represent this TrieNode in Java. We'll define it as a static inner class within our main Trie class for encapsulation.
class Trie {
static class TrieNode {
TrieNode[] children;
boolean isEndOfWord;
public TrieNode() {
children = new TrieNode[26]; // For 'a' through 'z'
isEndOfWord = false;
}
}
private final TrieNode root;
public Trie() {
root = new TrieNode();
}
// We will implement insert(), search(), and startsWith() here.
}
3. Visualizing the Core Operations
Before we jump into coding, let's build a strong mental model of how insert, search, and startsWith work. The following video provides an excellent walkthrough of these operations.
L1. Implement TRIE | INSERT | SEARCH | STARTSWITH
Watch this segment from the 'take U forward' channel. It provides a detailed, step-by-step visualization of how a Trie is built by inserting words and how search operations traverse the structure.
Please watch from 01:58 to 18:35. The video explains: The structure of a Trie node (01:58 - 04:00). How to insert words like 'apple', 'apps', etc., and see the tree grow (04:00 - 13:12). How to search for a full word and why the 'is end' flag is critical (13:12 - 16:18). How to check for a prefix (16:18 - 18:35).
4. Implementing the Trie in Java
Now, let's translate the concepts from the video into a practical Java implementation. We will implement three core methods: insert, search, and startsWith. While the learning outcome focuses on insert and search, startsWith is fundamental to a Trie's purpose and is a natural extension we'll need for our next lesson.
The following resource provides a clean, well-explained Java implementation that is very close to what you'd be expected to write in a technical interview.
208. Implement Trie (Prefix Tree) - In-Depth Explanation
Let's study the Java implementation from '208. Implement Trie (Prefix Tree) - In-Depth Explanation' on Algo.monster. It uses a clean, object-oriented approach.
Focus on the Java code block in this section. Notice how it defines the Trie class with its children array and isEndOfWord flag. Pay close attention to the insert logic and the reusable searchPrefix helper method.
Let's break down the logic for each method based on that implementation.
Insertion (insert)
To insert a word, we traverse the Trie from the root, character by character.
- Start with a
currentNodepointer at theroot. - For each
charin theword:- Calculate its
index(char - 'a'). - If
currentNode.children[index]isnull, it means this path doesn't exist yet. Create a newTrieNodeat that position:currentNode.children[index] = new TrieNode();. - Move the
currentNodeto its child:currentNode = currentNode.children[index];.
- Calculate its
- After the loop finishes, the
currentNodeis at the node corresponding to the last character of the word. Mark it as the end of a word:currentNode.isEndOfWord = true;.
// Inside the Trie class
public void insert(String word) {
TrieNode currentNode = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (currentNode.children[index] == null) {
currentNode.children[index] = new TrieNode();
}
currentNode = currentNode.children[index];
}
currentNode.isEndOfWord = true;
}
Search (search and startsWith)
Both search and startsWith involve traversing the Trie. We can create a private helper method, searchPrefix, to handle this traversal, which makes our code cleaner and avoids repetition. This helper will return the node at the end of the prefix, or null if the prefix doesn't exist.
Helper Method: searchPrefix
// Inside the Trie class
private TrieNode searchPrefix(String word) {
TrieNode currentNode = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (currentNode.children[index] == null) {
return null; // Path does not exist
}
currentNode = currentNode.children[index];
}
return currentNode; // Return the node at the end of the path
}
Now, search and startsWith become very simple.
-
search(String word): For a word to exist, its entire path must exist, and the final node must be marked as the end of a word.public boolean search(String word) { TrieNode node = searchPrefix(word); return node != null && node.isEndOfWord; } -
startsWith(String prefix): For a prefix to exist, its entire path must simply exist. We don't care about theisEndOfWordflag.public boolean startsWith(String prefix) { TrieNode node = searchPrefix(prefix); return node != null; }
Test your understanding!
Imagine you have an empty Trie. Trace the state of the Trie after the following operations are performed in order:
insert("hello")insert("help")search("hell")startsWith("hell")search("help")
What is the result of operations 3, 4, and 5?
Show answer
insert("hello"): Creates a pathh -> e -> l -> l -> o. The node for 'o' hasisEndOfWord = true.insert("help"): The pathh -> e -> lalready exists. It then adds a new branchl -> p. The node for 'p' hasisEndOfWord = true. The node for 'l' (the second 'l') now has two children: 'l' and 'p'.search("hell"): The pathh -> e -> l -> lexists, but theisEndOfWordflag for the second 'l' node isfalse(because we only inserted "hello", not "hell"). Result:false.startsWith("hell"): The pathh -> e -> l -> lexists. Since we are only checking for a prefix, theisEndOfWordflag doesn't matter. Result:true.search("help"): The pathh -> e -> l -> pexists, and theisEndOfWordflag for the 'p' node istrue. Result:true.
5. Complexity Analysis
Understanding the performance of a Trie is crucial for knowing when to use it.
-
Time Complexity:
insert(word): , where is the length of the word. We iterate through the word once.search(word): , where is the length of the word. We traverse a path of length .startsWith(prefix): , where is the length of the prefix.
-
Space Complexity:
- The space complexity is , where is the number of words and is the average length of a word. However, because prefixes are shared, the actual space is closer to . Each node takes up constant space (an array of 26 pointers and a boolean).
Conclusion
In this lesson, you've learned how to implement one of the most important string-based data structures. The Trie's ability to efficiently handle prefix-based queries makes it a go-to solution for many problems you'll encounter in interviews and competitive programming.
Key Takeaways:
- A Trie is a tree where paths from the root represent prefixes and words.
- Each Trie node contains an array of children (e.g., size 26 for lowercase letters) and a boolean flag (
isEndOfWord) to mark the end of a complete word. - The
insert,search, andstartsWithoperations all have a time complexity linear in the length of the input string, which is highly efficient. - Reusing code with helper methods (like
searchPrefix) is good practice for cleaner and more maintainable implementations.
Preview of the Next Lesson:
Now that we have a fully functional Trie, we can start applying it to solve real problems. In our next lesson, we will implement a word search and autocomplete feature using a Trie. This will build directly on the insert and startsWith methods we've just created, showing you a practical and powerful application of this data structure.