前缀树(字典树)

本文主要介绍前缀树的概念以及其引用, 并且提供C++的前缀树实现.

概念介绍

前缀树(Trie)也称为字典树, 其基本结构是N叉树, 常用来存储字符串, 也可存储其他的数据类型. 前缀树中根节点不存储前缀(字符串), 其余每一个节点存储一个前缀, 一个节点对应的字符串是通过该节点的所有前缀组成的字符串.

前缀树名称的由来是树中任意节点的所有后代有共同的前缀.

前缀树有着广泛的应用, 例如自动补全, 拼写检查, 词频统计, IP 路由 (最长前缀匹配),

复杂度分析

再分析一下时间复杂度, n为输入字符串的长度, m为已插入字符串的数量.

  • 插入字符串: O(n)
  • 查询某字符串或某前缀: O(n)

与其他数据结构的对比

也有其他的数据结构可以用来存储字符串, 例如哈希表, 平衡树.

  • 哈希表

    • 哈希表可以在O(1)内查询特定字符串, 但在表增大之后会出现大量的冲突,使其时间复杂度增加到O(m), m为已插入字符串的数量

    • 前缀树在存储有相同前缀的单词时会节省存储空间

    • 哈希表无法高效的找到有相同前缀的所有字符串

    • 哈希表无法按字典序枚举所有字符串

  • 平衡树

    • 平衡树中查找字符串的时间复杂度为 O(mlogn)

前缀树实现 (C++)

这里实现的前缀树只处理26个小写英文字母组成的串, 可以方便的扩展到更大的范围.

下面代码中实现了三个方法, 分别是:

  1. 插入字符串
  2. 查询字符串的数量
  3. 查询有某前缀的所有字符串的数量

此外, 本文这里使用定长数组来存储子节点的指针, 每个节点只存储1个字符, 其运算效率高, 但在字符集较大时会存在很大的存储开销. 另一种思路是利用哈希表存储前缀和对应子节点的关系, 这样可以节省空间.

此外还可以补充实现其他一些方法:

  • 删除字符串
  • 查询具有某前缀的所有字符串
  • 按照字典序枚举所有字符串
// 前缀树
#include <iostream>
#include <vector>
#include <string>

using namespace std;

class TrieNode{

private:

    int count;
    int prefix;

    TrieNode* nextNode[26];

public:

    TrieNode(){
        this->count=0;  // 以当前节点为结尾的单词数量
        this->prefix=0; // 包含该前缀的单词数量
    }

    // 插入新单词
    void insert(string str){
        TrieNode* root = this;
        // 注意:this指针可能为nullptr
        if(root==nullptr || str.empty()){
            return;
        }
        for(int i=0; i<str.size(); i++){
            if(root->nextNode[str[i]-'a']==nullptr){
                root->nextNode[str[i]-'a'] = new TrieNode();
            }
            root = root->nextNode[str[i]-'a'];
            root->prefix++;
        }
        root->count++;
    }

    // 查询单词的数量
    int search(string str){
        TrieNode* root = this;
        if(root==nullptr || str.empty()){
            return 0;
        }
        for(int i=0; i<str.size(); i++){
            if(root->nextNode[str[i]-'a']==nullptr){
                return 0;
            }
            root = root->nextNode[str[i]-'a'];
        }
        return root->count;
    }

    // 查询以str为前缀的单词的数量
    int searchPrefix(string str){
        TrieNode* root = this;
        if(root==nullptr || str.empty()){
            return 0;
        }
        for(int i=0; i<str.size(); i++){
            if(root->nextNode[str[i]-'a']==nullptr){
                return 0;
            }
            root = root->nextNode[str[i]-'a'];
        }
        return root->prefix;
    }

};


int main(){
    TrieNode* trie = new TrieNode();
    vector<string> test = {
        "hello", "hello", "helloworld", "", "hello",
    };
    for(string t: test){
        trie->insert(t);
    }
    cout<<trie->search("hello")<<" "<<trie->searchPrefix("hello")<<'\n';
    return 0;
}

LeetCode相关题目

208. 实现 Trie (前缀树) - 力扣(LeetCode)

472. 连接词 - 力扣(LeetCode)

677. 键值映射 - 力扣(LeetCode)

692. 前K个高频单词 - 力扣(LeetCode)

1032. 字符流 - 力扣(LeetCode)

参考

数据结构与算法:字典树(前缀树) - 知乎 (zhihu.com)

深入了解前缀树(超详细图文解释,含完整代码实现) - 掘金 (juejin.cn)

数据结构丨前缀树 - vincent1997 - 博客园 (cnblogs.com)

06-09 11:22