Your Progress

21/30 completed

70% complete. Continue learning to master DSA!

Tries (Prefix Trees)

A trie is a tree-like data structure that efficiently stores and retrieves keys in a dataset of strings. Also known as prefix trees, tries excel at tasks like autocomplete, spell checking, and IP routing.

Key Takeaways

  • Tries provide O(m) lookup time where m is the length of the key
  • They efficiently store strings with common prefixes to save space
  • Each node in a trie may contain up to 26 children (for English alphabet) or more
  • Tries excel at prefix matching, making them perfect for autocomplete features
  • They're more efficient than hash tables for specific string operations like prefix searches

What is a Trie?

A trie (pronounced "try" or "tree") is a specialized tree-based data structure that is used to store a dynamic set or associative array where the keys are usually strings. Unlike a binary search tree, no node in the tree stores the key associated with that node; instead, its position in the tree defines the key with which it is associated.

The name "trie" comes from the word "retrieval," highlighting its primary purpose: fast retrieval of stored data. Tries are also known as prefix trees, digital trees, or radix trees (although radix trees are a more specific type of trie).

Key Characteristics of Tries

  • Root Node: The root node of a trie is typically empty
  • Branching: Each node may have multiple children, one for each possible character
  • Path to Node: The path from the root to a node represents a prefix of one or more strings
  • End Markers: Special markers (or flags) indicate when a complete word/string is formed
  • Space Efficiency: Common prefixes are stored only once, saving memory

Trie Structure and Representation

A trie consists of nodes connected by edges. Each node in a trie represents a single character, and edges connect characters that appear sequentially in stored strings.

Basic Structure

Here's what a trie node typically contains:

  • An array or map of child nodes (one for each possible character)
  • A boolean flag indicating if the node represents the end of a valid word

Visually, a trie storing the words "car", "cat", "card", and "care" would look like this:

        (root)
           |
           c
           |
           a
          / \
         r   t
        /|\
       d  e

In this trie, following the path from root → c → a → r would represent the prefix "car". The nodes for 'd', 'e', and 't' would be marked as word endpoints.

Node Implementation

A typical trie node class or structure in JavaScript:

class TrieNode {
  constructor() {
    // Children map with character as key and TrieNode as value
    this.children = new Map();
    
    // Flag to indicate if this node represents the end of a word
    this.isEndOfWord = false;
  }
}

class Trie {
  constructor() {
    // Root of the trie
    this.root = new TrieNode();
  }
  
  // Other methods will be added here
}

Some implementations may use a fixed-size array for the children (e.g., size 26 for lowercase English letters), while others use a hash map or object for flexibility and to handle larger character sets.

Basic Trie Operations

A trie supports several standard operations that make it useful for string manipulation and searching:

1. Insertion

To insert a string into a trie, we start at the root and follow these steps for each character:

  1. Check if a node for the current character exists
  2. If not, create a new node for this character
  3. Move to the next character and the corresponding node
  4. After processing all characters, mark the last node as the end of a word
// Insert a word into the trie
insert(word) {
  let current = this.root;
  
  for (let i = 0; i < word.length; i++) {
    const char = word[i];
    
    // If the character doesn't exist in the children map, add it
    if (!current.children.has(char)) {
      current.children.set(char, new TrieNode());
    }
    
    // Move to the next node
    current = current.children.get(char);
  }
  
  // Mark the end of the word
  current.isEndOfWord = true;
}

2. Search

To search for a word, we follow a similar process as insertion but without creating new nodes:

// Search for a word in the trie
search(word) {
  let current = this.root;
  
  for (let i = 0; i < word.length; i++) {
    const char = word[i];
    
    // If the character doesn't exist in the children map, the word is not in the trie
    if (!current.children.has(char)) {
      return false;
    }
    
    // Move to the next node
    current = current.children.get(char);
  }
  
  // Return true only if the current node is marked as the end of a word
  return current.isEndOfWord;
}

3. Prefix Search

One of the key advantages of tries is the ability to efficiently find all words with a given prefix:

// Check if there is any word in the trie that starts with the given prefix
startsWith(prefix) {
  let current = this.root;
  
  for (let i = 0; i < prefix.length; i++) {
    const char = prefix[i];
    
    // If the character doesn't exist in the children map, no word with this prefix exists
    if (!current.children.has(char)) {
      return false;
    }
    
    // Move to the next node
    current = current.children.get(char);
  }
  
  // If we reach here, the prefix exists in the trie
  return true;
}

4. Deletion

Deletion from a trie is more complex than the other operations. We need to:

  • Find the node representing the last character of the word
  • Mark it as not the end of a word (if needed)
  • Remove nodes that are not used by any other word (to save space)

Deletion is typically implemented recursively, working from the end of the word back to the root.

Complete Trie Implementation

Here's a complete JavaScript implementation of a Trie with all the basic operations:

class TrieNode {
  constructor() {
    this.children = new Map();
    this.isEndOfWord = false;
  }
}

class Trie {
  constructor() {
    this.root = new TrieNode();
  }

  // Insert a word into the trie
  insert(word) {
    let current = this.root;
    
    for (let i = 0; i < word.length; i++) {
      const char = word[i];
      
      if (!current.children.has(char)) {
        current.children.set(char, new TrieNode());
      }
      
      current = current.children.get(char);
    }
    
    current.isEndOfWord = true;
  }

  // Search for a word in the trie
  search(word) {
    let current = this.root;
    
    for (let i = 0; i < word.length; i++) {
      const char = word[i];
      
      if (!current.children.has(char)) {
        return false;
      }
      
      current = current.children.get(char);
    }
    
    return current.isEndOfWord;
  }

  // Check if there is any word in the trie that starts with the given prefix
  startsWith(prefix) {
    let current = this.root;
    
    for (let i = 0; i < prefix.length; i++) {
      const char = prefix[i];
      
      if (!current.children.has(char)) {
        return false;
      }
      
      current = current.children.get(char);
    }
    
    return true;
  }

  // Delete a word from the trie
  delete(word) {
    this._deleteRecursive(this.root, word, 0);
  }

  // Helper method for delete
  _deleteRecursive(current, word, index) {
    // Base case: end of word reached
    if (index === word.length) {
      // Word exists in trie, so unmark the end of word
      if (current.isEndOfWord) {
        current.isEndOfWord = false;
      }
      
      // Return true if node has no children (can be deleted)
      return current.children.size === 0;
    }

    const char = word[index];
    const node = current.children.get(char);
    
    // Word not found in trie
    if (!node) {
      return false;
    }

    // Recursively delete for next character
    const shouldDeleteNode = this._deleteRecursive(node, word, index + 1);

    // If child node can be deleted, remove it from the map
    if (shouldDeleteNode) {
      current.children.delete(char);
      
      // Current node can be deleted if it's not the end of another word
      // and has no other children
      return !current.isEndOfWord && current.children.size === 0;
    }

    return false;
  }
}

// Example usage
const trie = new Trie();
trie.insert("apple");
trie.insert("app");
trie.insert("application");

console.log(trie.search("apple"));      // true
console.log(trie.search("app"));        // true
console.log(trie.search("appl"));       // false
console.log(trie.startsWith("app"));    // true

trie.delete("app");
console.log(trie.search("app"));        // false
console.log(trie.search("apple"));      // true
console.log(trie.startsWith("app"));    // true

Applications of Tries

Tries are extremely useful in a variety of applications that involve string processing:

Autocomplete Systems

Tries can quickly find all words that start with a given prefix, making them ideal for search suggestions and autocomplete features.

Spell Checkers

Tries can verify if a word exists in a dictionary and suggest corrections for misspelled words.

IP Routing

PATRICIA tries (a variant of tries) are used in network routers for efficient IP address lookup.

Text Processing

Used in implementing data compression algorithms, text search algorithms like Aho-Corasick, and more.

Predictive Text

Mobile keyboards use tries to predict the next word based on input patterns.

Longest Prefix Matching

Efficient algorithms for finding the longest matching prefix among a set of strings.

Time and Space Complexity

OperationTime ComplexityNotes
InsertionO(m)Where m is the length of the key
SearchO(m)Where m is the length of the key
Prefix SearchO(p)Where p is the length of the prefix
DeletionO(m)Where m is the length of the key

Space Complexity

The space complexity of a trie is more complex to analyze:

  • Worst case: O(n × m), where n is the number of keys and m is the length of the longest key
  • Best case: O(n) when all keys share most of their prefixes
  • Each node may need to store references to up to 26 children (for English alphabet) or more for larger character sets

While tries can be memory-intensive, they offer trade-offs with speed that make them valuable for many applications.

Conclusion

Tries are powerful data structures that excel at string operations, particularly those involving prefixes. While they may consume more memory than some alternatives, their speed and flexibility make them an essential tool for any programmer working with string data.

Remember

When deciding whether to use a trie, consider:

  • Whether prefix operations are important to your application
  • The memory constraints of your system
  • The trade-off between time and space complexity
  • Whether you need to support additional operations like spell-checking or autocomplete

Next Steps

To deepen your understanding of tries and related data structures, explore these topics:

Related Tutorials

Related Tutorials

Hash Tables

Compare Tries with another efficient data structure for string operations.

Learn more

Trees

Learn about the foundational data structure behind Tries.

Learn more

String Algorithms

Explore more advanced algorithms for string processing.

Learn more