Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 61

Implementing Tries

Recommended resources

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:

Trie Data Structure Example
A Trie storing the words "and", "ant", "dad", and "do". Notice how "and" and "ant" share the path `a -> n`. The dark blue nodes mark the end of a complete word.

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:

  1. 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 character c with the expression c - 'a'.
  2. 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.

  1. Start with a currentNode pointer at the root.
  2. For each char in the word:
    • Calculate its index (char - 'a').
    • If currentNode.children[index] is null, it means this path doesn't exist yet. Create a new TrieNode at that position: currentNode.children[index] = new TrieNode();.
    • Move the currentNode to its child: currentNode = currentNode.children[index];.
  3. After the loop finishes, the currentNode is 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 the isEndOfWord flag.

    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:

  1. insert("hello")
  2. insert("help")
  3. search("hell")
  4. startsWith("hell")
  5. search("help")

What is the result of operations 3, 4, and 5?

Show answer
  1. insert("hello"): Creates a path h -> e -> l -> l -> o. The node for 'o' has isEndOfWord = true.
  2. insert("help"): The path h -> e -> l already exists. It then adds a new branch l -> p. The node for 'p' has isEndOfWord = true. The node for 'l' (the second 'l') now has two children: 'l' and 'p'.
  3. search("hell"): The path h -> e -> l -> l exists, but the isEndOfWord flag for the second 'l' node is false (because we only inserted "hello", not "hell"). Result: false.
  4. startsWith("hell"): The path h -> e -> l -> l exists. Since we are only checking for a prefix, the isEndOfWord flag doesn't matter. Result: true.
  5. search("help"): The path h -> e -> l -> p exists, and the isEndOfWord flag for the 'p' node is true. 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, and startsWith operations 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.