Implement Trie (Prefix Tree)

medium

A trie (pronounced "try") or prefix tree is a tree data structure used to efficiently store and retrieve keys in a dataset of strings. Implement the Trie class with the following operations:

  • Trie() Initializes the trie object.
  • void insert(String word) Inserts the string word into the trie.
  • boolean search(String word) Returns true if the string word is in the trie (i.e., was inserted before), and false otherwise.
  • boolean startsWith(String prefix) Returns true if there is a previously inserted string word that has the prefix prefix, and false otherwise.

Examples

Example 1

  • Input: ["Trie", "insert", "search", "search", "startsWith", "insert", "search"], [[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
  • Output: [null, null, true, false, true, null, true]
  • Explanation: After inserting "apple", searching "apple" returns true. Searching "app" returns false since it was not inserted. However, startsWith("app") returns true because "apple" starts with "app". After inserting "app", searching "app" returns true.

Constraints

  • 1 <= word.length, prefix.length <= 2000
  • word and prefix consist only of lowercase English letters.
  • At most 3 * 10^4 calls in total will be made to insert, search, and startsWith.

Approaches

2 worked solutions ranked from straightforward to optimal — each with intuition, algorithm, complexity, and verified code in C++, Java, Python, and Go.

Approach 1

Array-Based Trie Node

Intuition

The classic trie implementation uses an array of 26 pointers (one for each lowercase letter) at each node, plus a boolean flag to mark the end of a word. To insert a word, we traverse from the root, creating new nodes as needed for each character. To search, we follow the character path and check the end-of-word flag. For prefix search, we follow the same path but do not require the end-of-word flag. This approach offers O(1) character lookups at each level at the cost of higher memory usage.

Algorithm

  1. Each trie node has an array of 26 child pointers (initialized to null) and an `isEnd` boolean.
  2. **Insert:** For each character in the word, compute its index (char - 'a'). If the child at that index is null, create a new node. Move to the child. After the last character, mark the node as end-of-word.
  3. **Search:** For each character, follow the child pointer. If any is null, return false. After the last character, return the isEnd flag.
  4. **StartsWith:** Same as search, but return true if we reach the end of the prefix without encountering a null pointer (regardless of isEnd).
TimeO(m) per operation where m is the length of the word or prefixSpaceO(n * m) where n is the number of words and m is the average word length

Code (C++ · Java · Python · Go)

C++17
class Trie {
private:
    struct TrieNode {
        TrieNode* children[26] = {};
        bool isEnd = false;
    };

    TrieNode* root;

public:
    Trie() {
        root = new TrieNode();
    }

    void insert(string word) {
        TrieNode* node = root;
        for (char c : word) {
            int idx = c - 'a';
            if (node->children[idx] == nullptr) {
                node->children[idx] = new TrieNode();
            }
            node = node->children[idx];
        }
        node->isEnd = true;
    }

    bool search(string word) {
        TrieNode* node = findNode(word);
        return node != nullptr && node->isEnd;
    }

    bool startsWith(string prefix) {
        return findNode(prefix) != nullptr;
    }

private:
    TrieNode* findNode(const string& s) {
        TrieNode* node = root;
        for (char c : s) {
            int idx = c - 'a';
            if (node->children[idx] == nullptr) return nullptr;
            node = node->children[idx];
        }
        return node;
    }
};
Java 21
class Trie {
    private int[][] children;
    private boolean[] isEnd;
    private int count;

    public Trie() {
        children = new int[60001][26];
        isEnd = new boolean[60001];
        count = 0;
        for (int[] row : children) {
            Arrays.fill(row, -1);
        }
    }

    public void insert(String word) {
        int node = 0;
        for (char c : word.toCharArray()) {
            int idx = c - 'a';
            if (children[node][idx] == -1) {
                count++;
                children[node][idx] = count;
            }
            node = children[node][idx];
        }
        isEnd[node] = true;
    }

    public boolean search(String word) {
        int node = findNode(word);
        return node != -1 && isEnd[node];
    }

    public boolean startsWith(String prefix) {
        return findNode(prefix) != -1;
    }

    private int findNode(String s) {
        int node = 0;
        for (char c : s.toCharArray()) {
            int idx = c - 'a';
            if (children[node][idx] == -1) return -1;
            node = children[node][idx];
        }
        return node;
    }
}
Python 3.12
class Trie:
    def __init__(self):
        self.children = {}
        self.is_end = False

    def insert(self, word: str) -> None:
        node = self
        for c in word:
            if c not in node.children:
                node.children[c] = Trie()
            node = node.children[c]
        node.is_end = True

    def search(self, word: str) -> bool:
        node = self._find(word)
        return node is not None and node.is_end

    def startsWith(self, prefix: str) -> bool:
        return self._find(prefix) is not None

    def _find(self, s: str):
        node = self
        for c in s:
            if c not in node.children:
                return None
            node = node.children[c]
        return node
Go 1.26
type Trie struct {
    children [26]*Trie
    isEnd    bool
}

func Constructor() Trie {
    return Trie{}
}

func (t *Trie) Insert(word string) {
    node := t
    for _, c := range word {
        idx := c - 'a'
        if node.children[idx] == nil {
            node.children[idx] = &Trie{}
        }
        node = node.children[idx]
    }
    node.isEnd = true
}

func (t *Trie) Search(word string) bool {
    node := t.find(word)
    return node != nil && node.isEnd
}

func (t *Trie) StartsWith(prefix string) bool {
    return t.find(prefix) != nil
}

func (t *Trie) find(s string) *Trie {
    node := t
    for _, c := range s {
        idx := c - 'a'
        if node.children[idx] == nil {
            return nil
        }
        node = node.children[idx]
    }
    return node
}
Approach 2

HashMap-Based Trie Node

Intuition

Instead of using a fixed-size array for children, we can use a hash map at each node. This trades slightly slower lookups for more memory-efficient nodes when the alphabet is large or nodes are sparse. The logic is identical to the array-based approach: traverse the trie character by character, creating nodes on insert, and checking for existence on search. The hash map approach is also more flexible, supporting arbitrary character sets without changing the node structure.

Algorithm

  1. Each trie node has a hash map mapping characters to child nodes and an `isEnd` boolean.
  2. **Insert:** For each character, if it is not in the current node's map, add a new child node. Move to the child. Mark the last node as end-of-word.
  3. **Search:** For each character, check if it exists in the map. If not, return false. After the last character, return the isEnd flag.
  4. **StartsWith:** Same as search, but return true after reaching the end of the prefix.
TimeO(m) per operation where m is the length of the word or prefixSpaceO(n * m) where n is the number of words and m is the average word length

Code (C++ · Java · Python · Go)

C++17
class Trie {
private:
    struct TrieNode {
        unordered_map<char, TrieNode*> children;
        bool isEnd = false;
    };

    TrieNode* root;

public:
    Trie() {
        root = new TrieNode();
    }

    void insert(string word) {
        TrieNode* node = root;
        for (char c : word) {
            if (node->children.find(c) == node->children.end()) {
                node->children[c] = new TrieNode();
            }
            node = node->children[c];
        }
        node->isEnd = true;
    }

    bool search(string word) {
        TrieNode* node = findNode(word);
        return node != nullptr && node->isEnd;
    }

    bool startsWith(string prefix) {
        return findNode(prefix) != nullptr;
    }

private:
    TrieNode* findNode(const string& s) {
        TrieNode* node = root;
        for (char c : s) {
            if (node->children.find(c) == node->children.end()) return nullptr;
            node = node->children[c];
        }
        return node;
    }
};
Java 21
class Trie {
    private Map<Character, Trie> children;
    private boolean isEnd;

    public Trie() {
        children = new HashMap<>();
        isEnd = false;
    }

    public void insert(String word) {
        Trie node = this;
        for (char c : word.toCharArray()) {
            node.children.putIfAbsent(c, new Trie());
            node = node.children.get(c);
        }
        node.isEnd = true;
    }

    public boolean search(String word) {
        Trie node = find(word);
        return node != null && node.isEnd;
    }

    public boolean startsWith(String prefix) {
        return find(prefix) != null;
    }

    private Trie find(String s) {
        Trie node = this;
        for (char c : s.toCharArray()) {
            if (!node.children.containsKey(c)) return null;
            node = node.children.get(c);
        }
        return node;
    }
}
Python 3.12
class Trie:
    def __init__(self):
        self.children = {}
        self.is_end = False

    def insert(self, word: str) -> None:
        node = self
        for c in word:
            if c not in node.children:
                node.children[c] = Trie()
            node = node.children[c]
        node.is_end = True

    def search(self, word: str) -> bool:
        node = self._find(word)
        return node is not None and node.is_end

    def startsWith(self, prefix: str) -> bool:
        return self._find(prefix) is not None

    def _find(self, s: str):
        node = self
        for c in s:
            if c not in node.children:
                return None
            node = node.children[c]
        return node
Go 1.26
type Trie struct {
    children map[rune]*Trie
    isEnd    bool
}

func Constructor() Trie {
    return Trie{children: make(map[rune]*Trie)}
}

func (t *Trie) Insert(word string) {
    node := t
    for _, c := range word {
        if _, ok := node.children[c]; !ok {
            node.children[c] = &Trie{children: make(map[rune]*Trie)}
        }
        node = node.children[c]
    }
    node.isEnd = true
}

func (t *Trie) Search(word string) bool {
    node := t.find(word)
    return node != nil && node.isEnd
}

func (t *Trie) StartsWith(prefix string) bool {
    return t.find(prefix) != nil
}

func (t *Trie) find(s string) *Trie {
    node := t
    for _, c := range s {
        if _, ok := node.children[c]; !ok {
            return nil
        }
        node = node.children[c]
    }
    return node
}

Frequently asked questions

What is the optimal time complexity of Implement Trie (Prefix Tree)?

The most efficient approach in this guide ("HashMap-Based Trie Node") runs in O(m) per operation where m is the length of the word or prefix time and uses O(n * m) where n is the number of words and m is the average word length extra space. Earlier brute-force approaches are slower; see all 2 approaches above for the full progression.

What pattern does Implement Trie (Prefix Tree) use?

Implement Trie (Prefix Tree) is a medium-level Tries problem involving Trie, String, Design. Recognizing the pattern is half the work — see the intuition section above for how to spot it.

Can Implement Trie (Prefix Tree) be solved without extra space?

The most space-efficient approach in this guide uses O(n * m) where n is the number of words and m is the average word length extra space (excluding the input). If you're aiming for in-place — see the trade-off in the algorithm section above — the brute-force approach uses O(n * m) where n is the number of words and m is the average word length, while the optimized version uses O(n * m) where n is the number of words and m is the average word length.

Which programming languages does this Ratta solution support?

Every approach above ships with verified, runnable solutions in C++, Java, Python, and Go. The starter templates in the workspace match the same four languages so you can practice in your interview language of choice.

Original problem: leetcode.com

Loading editor...
operations=
["Trie","insert","search","search","startsWith","insert","search"]
arguments=
[[],["apple"],["apple"],["app"],["app"],["app"],["app"]]
[null,null,true,false,true,null,true]