基础算法--Trie

基础算法–Trie树

trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。 – 维基百科

存储多个字符串

暴力存储多个字符串,每次读入一个串就相应地开辟存储空间。但如果这些字符串中有大量重复串,我们又该如何节省空间

Root
Diospyros_kaki
Diospyros_lotus
Dioscorea_polystachya
Prunus_persica
Prunus_pseudocerasus
Prunus_mume

上面字符串其实又一些共同前缀,因此我们可以将上面结构进一步压缩

Root
Diospyros_
kaki
lotus
Dioscorea_polystachya
Prunus_
p
ersica
seudocerasus
mume

其实上面的结构已经和我们传统定义的Trie已经很接近了,只需要解决最后一个问题,如何组织子节点

构造Trie

传统定义的Trie需要满足三个条件:

  • 根节点不包含字符,除根节点外每一个节点都只包含一个字符
  • 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
  • 每个节点的所有子节点包含的字符都不相同

节点

struct TrieNode {
    char val;			// 节点字符
    int end_cnt;		// 以val字符为结尾的字符串的个数
    int pre_cnt;		// 以val字符为前缀的字符串的个数
    std::map<char, TrieNode *> chidren;
    TrieNode(char v) {
        end_cnt = 0;
        pre_cnt = 0;
        val = v;
    }
};

我们将根节点的val设置为空,根节点是每个字符串的前缀,因此根节点的pre_cnt即为Trie中字符串的个数

插入字符串

void insert(TrieNode *root, std::string &str) {
    if (!root || str.empty()) return;
    for (size_t i = 0; i < str.size(); ++i) {
        if (root->chidren.find(str[i]) == root->chidren.end()) {
            root->chidren.insert(std::make_pair(str[i], new TrieNode(str[i])));
        }
        root->pre_cnt += 1;
        root = root->chidren[str[i]];
    }
    root->end_cnt += 1;
}

查找

在不考虑hash的情况下,Trie对字符串的查找效率还是非常高的。不管集合是多大,查到字符串的时间复杂度都是 O ( n ) O(n) O(n)其中 n n n为要查找字符串的长度

bool find(TrieNode *root, std::string &str) {
    if (!root) return false;
    for (size_t i = 0; i < str.size(); ++i) {
        if (root->chidren.find(str[i]) == root->chidren.end()) return false;
        root = root->chidren[str[i]];
    }
    return root->end_cnt > 0;
}

释放

因为我们使用了堆内存,因此别忘了释放内存

void free(TrieNode *root) {
    for (auto &it : root->chidren) {
        free(it.second);
    }
    root->chidren.clear();
    delete root;
}

显然,如果数据字符串数据量非常大,并且这些字符串中有大量重复串,我们使用Trie这种结构存储可以大量节省内存。当然有些时候对每个字符都建立一个节点也会照成一些空间上的浪费,因此在实际使用中往往会根据具体实际情况对Trie中节点做一些合并和压缩,这里就不展开了,有兴趣的同学可以查阅相关资料

应用举例

前缀匹配

给定一个字符串集合 N N N,如果我想获取指定前缀的所有字符串集合 R ( R ⊂ N ) R(R\subset N) R(RN)的数量。很显然暴力求解的话时间复杂度是 O ( h ∗ N ) O(h*N) O(hN)其中 h h h为要查找前缀的长度。那么用Trie就不一样了,它可以做到 O ( h ) O(h) O(h)

int find_prefix_cnt(TrieNode *root, std::string &prefix) {
    if (!root) return 0;
    for (size_t i = 0; i < prefix.size(); ++i) {
        if (root->chidren.find(prefix[i]) == root->chidren.end()) return 0;
        root = root->chidren[prefix[i]];
    }
    return root->pre_cnt;
}

这个在我们词典中对单词的检索其实是非常有用的,当然,我们还可以数据匹配到的字符串集合,理解原理之后其实也很简单,这里就不展开了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虎小黑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值