C++ Program to Implement a Trie

Introduction

A Trie, also known as a prefix tree, is a specialized tree used to store associative data structures. Tries are especially useful for applications that involve searching for words in a dictionary, auto-completion, and spell checking. In this article, we will explore how to write a C++ Program to Implement a Trie with detailed explanations and real-world examples. By the end of this article, you will have a thorough understanding of Tries and their implementation in C++ projects.

Prerequisites

Before diving into the implementation of a Trie, it is important to have a basic understanding of the following concepts:

  • Tree Data Structures: A hierarchical data structure consisting of nodes with a parent-child relationship.
  • Basic C++ Programming: Familiarity with classes, pointers, and standard library components.
  • String Manipulation: Understanding of string operations in C++.

Make sure you are comfortable with these concepts to fully grasp the implementation of a Trie.

1. Understanding Tries

1.1 Properties of Tries

A Trie is a tree-like data structure that stores a dynamic set of strings, where the keys are usually strings. Tries have the following properties:

  1. Each node represents a single character of a string.
  2. The root node is an empty node.
  3. Each path from the root to a leaf node represents a word.

1.2 Why Use Tries?

Tries are particularly efficient for searching, inserting, and deleting words in a dictionary. The time complexity for these operations is O(m), where m is the length of the word. This efficiency makes Tries suitable for applications like auto-complete systems, spell checkers, and IP routing.

2. Implementing Trie in C++

Let’s go through three different examples to understand the implementation and usage of Tries in C++.

2.1 Basic Implementation

In this example, we will implement the basic structure of a Trie with insertion and search functionalities.

Code

C++
#include <iostream>
#include <unordered_map>
using namespace std;

class TrieNode {
public:
    unordered_map<char, TrieNode*> children;
    bool isEndOfWord;

    TrieNode() {
        isEndOfWord = false;
    }
};

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

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

    bool search(string word) {
        TrieNode* currentNode = root;
        for (char ch : word) {
            if (currentNode->children.find(ch) == currentNode->children.end()) {
                return false;
            }
            currentNode = currentNode->children[ch];
        }
        return currentNode->isEndOfWord;
    }
};

int main() {
    Trie trie;
    trie.insert("hello");
    trie.insert("world");

    cout << trie.search("hello") << endl; // Output: 1 (true)
    cout << trie.search("world") << endl; // Output: 1 (true)
    cout << trie.search("helloworld") << endl; // Output: 0 (false)

    return 0;
}

Output

C++
1
1
0

Explanation

In this example, we implement the basic structure of a Trie with insertion and search functionalities. The code includes a TrieNode class to represent each node of the Trie and a Trie class to handle insertion and searching of words. The output demonstrates the functionality of the Trie by searching for words that have been inserted and a word that has not been inserted.

2.2 Deletion in Trie

Next, we will extend our implementation to include deletion functionality.

Code

C++
#include <iostream>
#include <unordered_map>
using namespace std;

class TrieNode {
public:
    unordered_map<char, TrieNode*> children;
    bool isEndOfWord;

    TrieNode() {
        isEndOfWord = false;
    }
};

class Trie {
private:
    TrieNode* root;
    bool deleteHelper(TrieNode* currentNode, string word, int depth);

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

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

    bool search(string word) {
        TrieNode* currentNode = root;
        for (char ch : word) {
            if (currentNode->children.find(ch) == currentNode->children.end()) {
                return false;
            }
            currentNode = currentNode->children[ch];
        }
        return currentNode->isEndOfWord;
    }

    void deleteWord(string word) {
        deleteHelper(root, word, 0);
    }
};

bool Trie::deleteHelper(TrieNode* currentNode, string word, int depth) {
    if (depth == word.size()) {
        if (!currentNode->isEndOfWord) return false;
        currentNode->isEndOfWord = false;
        return currentNode->children.empty();
    }

    char ch = word[depth];
    TrieNode* node = currentNode->children[ch];
    if (!node) return false;

    bool shouldDeleteCurrentNode = deleteHelper(node, word, depth + 1);

    if (shouldDeleteCurrentNode) {
        currentNode->children.erase(ch);
        return currentNode->children.empty() && !currentNode->isEndOfWord;
    }
    return false;
}

int main() {
    Trie trie;
    trie.insert("hello");
    trie.insert("world");
    trie.insert("hell");

    cout << trie.search("hello") << endl; // Output: 1 (true)
    trie.deleteWord("hello");
    cout << trie.search("hello") << endl; // Output: 0 (false)
    cout << trie.search("hell") << endl; // Output: 1 (true)

    return 0;
}

Output

C++
1
0
1

Explanation

In this example, we extend the Trie implementation to include deletion functionality. The code includes a deleteHelper function that recursively deletes nodes and ensures the Trie remains intact after deletion. The output shows the successful deletion of a word and the subsequent search results, demonstrating the correct functionality of the Trie.

2.3 Real-World Application: Auto-Complete System

In this example, we will implement a simple auto-complete system using a Trie.

Code

C++
#include <iostream>
#include <unordered_map>
#include <vector>
using namespace std;

class TrieNode {
public:
    unordered_map<char, TrieNode*> children;
    bool isEndOfWord;

    TrieNode() {
        isEndOfWord = false;
    }
};

class Trie {
private:
    TrieNode* root;
    void findWords(TrieNode* currentNode, string prefix, vector<string>& results);

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

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

    vector<string> autoComplete(string prefix) {
        vector<string> results;
        TrieNode* currentNode = root;
        for (char ch : prefix) {
            if (currentNode->children.find(ch) == currentNode->children.end()) {
                return results;
            }
            currentNode = currentNode->children[ch];
        }
        findWords(currentNode, prefix, results);
        return results;
    }
};

void Trie::findWords(TrieNode* currentNode, string prefix, vector<string>& results) {
    if (currentNode->isEndOfWord) {
        results.push_back(prefix);
    }
    for (auto pair : currentNode->children) {
        findWords(pair.second, prefix + pair.first, results);
    }
}

int main() {
    Trie trie;
    trie.insert("hello");
    trie.insert("hell");
    trie.insert("heaven");
    trie.insert("heavy");

    vector<string> results = trie.autoComplete("he");
    for (string word : results) {
        cout << word << endl;
    }

    return 0;
}

Output

C++
heaven
heavy
hell
hello

Explanation

In this real-world example, we implement a simple auto-complete system using a Trie. The code includes a findWords function that recursively finds all words with a given prefix. The autoComplete function utilizes this to return a list of suggestions based on the input prefix. The output shows the auto-complete suggestions for the prefix “he”.

Conclusion

Tries are an efficient and powerful data structure for managing and searching strings. Understanding and implementing Tries in C++ can significantly enhance the performance of applications like auto-complete systems, spell checkers, and dictionaries. By following the examples provided in this article, you should now have a solid foundation for implementing and utilizing Tries in your projects