foreach-ui logo
codeLanguages
account_treeDSA

Quick Actions

quizlock Random Quiz
trending_uplock Progress
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • quiz
Algorithms
  • Understand how tries store strings as paths sharing common prefixes
  • Learn insert/search/delete operations and their O(m) complexity
  • See when tries beat hash maps: autocomplete, prefix search, enumeration

Trie Data Structure

You're building autocomplete for a search bar. Type "alg" and it suggests "algorithm", "algebra", "algae". How does it find all words starting with "alg" so fast?

A trie (pronounced "try") is the answer. It's a tree where each path from root to leaf spells out a word. Words sharing prefixes share the path for that prefix.

The Structure

Think of a trie like a decision tree for spelling words. At each node, you choose the next letter.

For the words "cat", "car", and "dog":

       root
      /    \
     c      d
     |      |
     a      o
    / \     |
   t   r    g
  (cat)(car)(dog)

Each node has:

  • Children (map from character to node)
  • A flag: is this the end of a word?
class TrieNode:
    def __init__(self):
        self.children = {}  # char -> TrieNode
        self.is_end_of_word = False

class Trie:
    def __init__(self):
        self.root = TrieNode()

Inserting a Word

To insert "cat":

  1. Start at root
  2. For 'c': if child 'c' exists, go there; else create it
  3. For 'a': same
  4. For 't': same
  5. Mark the 't' node as end of word
def insert(self, word):
    node = self.root
    for char in word:
        if char not in node.children:
            node.children[char] = TrieNode()
        node = node.children[char]
    node.is_end_of_word = True

After inserting "cat", "car", "dog":

root
├─ c
│  └─ a
│     ├─ t (END: "cat")
│     └─ r (END: "car")
└─ d
   └─ o
      └─ g (END: "dog")

Time: O(m) where m is word length. You visit each character once.

Space: O(m) in the worst case if no prefix is shared. But with shared prefixes, much better.

Searching for a Word

To search for "car":

  1. Start at root
  2. Follow 'c' → 'a' → 'r'
  3. Check if the 'r' node is marked end of word
def search(self, word):
    node = self.root
    for char in word:
        if char not in node.children:
            return False  # Path doesn't exist
        node = node.children[char]
    return node.is_end_of_word  # True only if word ends here

For "ca" in the trie above: we reach the 'a' node, but it's not marked end of word, so return False.

Time: O(m). Follow the path, check the flag.

Prefix Search

Check if any word starts with a prefix. For "ca", we'd find "cat" and "car".

def starts_with(self, prefix):
    node = self.root
    for char in prefix:
        if char not in node.children:
            return False  # Prefix doesn't exist
        node = node.children[char]
    return True  # We found the prefix path

Time: O(m) where m is prefix length.

Finding All Words with a Prefix

For autocomplete, you need all words starting with "ca":

def find_words_with_prefix(self, prefix):
    node = self.root
    
    # Navigate to prefix
    for char in prefix:
        if char not in node.children:
            return []  # No words with this prefix
        node = node.children[char]
    
    # DFS from this node to collect all words
    words = []
    self._dfs(node, prefix, words)
    return words

def _dfs(self, node, current_word, words):
    if node.is_end_of_word:
        words.append(current_word)
    
    for char, child in node.children.items():
        self._dfs(child, current_word + char, words)

For prefix "ca" in our trie:

  1. Navigate to the 'a' node
  2. DFS from there: find "cat" and "car"

Time: O(p + n) where p is prefix length and n is total characters in all matching words.

Deletion

Deleting is trickier. Remove "cat" from the trie:

def delete(self, word):
    def _delete(node, word, index):
        if index == len(word):
            # Reached end of word
            if not node.is_end_of_word:
                return False  # Word doesn't exist
            node.is_end_of_word = False
            # Delete this node only if it has no children
            return len(node.children) == 0
        
        char = word[index]
        if char not in node.children:
            return False  # Word doesn't exist
        
        child = node.children[char]
        should_delete_child = _delete(child, word, index + 1)
        
        if should_delete_child:
            del node.children[char]
            # Delete current node if it's not end of another word and has no children
            return not node.is_end_of_word and len(node.children) == 0
        
        return False
    
    _delete(self.root, word, 0)

Why the complexity? After deleting "cat", we still need "car". We can't delete the 'c' and 'a' nodes because they're part of "car". We only delete the 't' node.

But if we delete "dog" and it's the only word starting with 'd', we can delete the entire branch.

Space Efficiency

The naive implementation uses a hash map at each node. For lowercase letters, that's potentially 26 entries per node.

With many words, this adds up. For 10,000 words averaging 10 characters, you might have 100,000 nodes, each with a hash map.

Optimization: use an array of size 26 instead of a hash map (for lowercase letters):

class TrieNode:
    def __init__(self):
        self.children = [None] * 26  # a-z
        self.is_end_of_word = False

def char_to_index(char):
    return ord(char) - ord('a')

def insert(self, word):
    node = self.root
    for char in word:
        idx = char_to_index(char)
        if node.children[idx] is None:
            node.children[idx] = TrieNode()
        node = node.children[idx]
    node.is_end_of_word = True

Trade-off: fixed memory per node (26 pointers) vs dynamic memory (hash map grows as needed).

When Tries Shine

Autocomplete: Type "alg", get all words starting with "alg". Hash maps can't do this efficiently.

Spell checking: Store dictionary in a trie. Check if a word exists in O(m).

IP routing: Routers use tries for longest prefix matching on IP addresses.

Dictionary applications: Find all words with a given prefix for word games, predictive text, etc.

vs Hash Maps

Hash map: O(m) insert, O(m) search, O(1) for exact match (if hash is O(1)).

Trie: O(m) insert, O(m) search, but also O(m) prefix search and can enumerate all words with a prefix.

Hash maps can't do prefix operations efficiently. Tries can't beat O(1) for exact key lookup (though O(m) is often acceptable).

vs Binary Search Trees

BST: O(log n) search where n is number of words, but comparisons are O(m) each, so O(m log n) total.

Trie: O(m) search regardless of how many words are stored.

For string operations, tries are usually better.

The Trade-off

Tries use more memory than hash maps for the same data (all those node objects and child pointers).

But they enable operations that would otherwise be expensive: prefix search, enumeration, range queries on strings.

If you need prefix operations or autocomplete, the memory cost is worth it. If you only need exact lookups, a hash map is simpler and often faster.

Variations

Compressed trie (Patricia trie): Merge nodes with single children to save space.

Ternary search trie: Each node has 3 children (less than, equal, greater than) for better space efficiency.

Suffix trie: Store all suffixes of a string for substring search operations.

The basic trie is the foundation. Once you understand it, the variations make sense as optimizations for specific use cases.

© 2026 forEach. All rights reserved.

Privacy Policy•Terms of Service