Trie树

Trie树,即字典树,又称单词查找树。经常应用于字符串的统计与排序,经常被搜索引擎系统用于文本词频统计。

核心思想是:空间换时间,利用字符串的公共前缀,来降低查询时间的开销以达到提高效率的目的

一般有以下3个性质:

  • 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  • 从根节点到某一节点,路劲上经过的字符连接起来,为该节点对应的字符串。
  • 每个节点的所有子节点包含的字符都不相同。
Trie树_第1张图片
Trie树.png
/* 结点 */
const int ALPHABET = 26;   //26个小写字母
struct TrieNode{
    size_t n;
    TrieNode* childrens[ALPHABET];  // 指针数组

    TrieNode(size_t n_ = 0): n(n_) {
        for (int i = 0; i != ALPHABET; i++)
            childrens[i] = nullptr;
    }
};

/* Trie树 */
class Trie
{
    ......
    private:
        TrieNode* root;    // 根节点
        size_t count;      // 记录单词的数量 
};
查找

在单词查找树中,查找分为3类情况:

  • 键的尾字符所对应的结点中的频率非0。(命中查找)
  • 键的尾字符所对应的结点中的频率为0。(未命中查找)
  • 查找结束于一条空的链接。 (未命中查找)
Trie树_第2张图片
Trie查找.png
size_t Trie::find(const std::string &word) const {
    if (root == nullptr)
        return 0;
    
    TrieNodePtr cur = root;
    for (const auto& ch : word) {
        if ((cur->childrens)[ch-'a'] == nullptr)
            return 0;
        cur = (cur->childrens)[ch-'a'];
    }

    return cur->n;
}
插入

在插入之前要沿着进行一次查找,此时有可能出现2种情况:

  • 在到达键的尾字符之前就遇到了空的链接(没有"路径"),则要沿着新建节点建立"路径"
  • 在遇到空连接之前就到达了键的尾字符
Trie树_第3张图片
Trie插入.png
void Trie::insert(const std::string &word) {
    if (word.empty())
        return;

    if (root == nullptr)
        root = new TrieNode();

    TrieNodePtr cur = root;
    for (const auto& ch : word) {
        if ((cur->childrens)[ch-'a'] == nullptr)
            (cur->childrens)[ch - 'a'] = new TrieNode();
        cur = (cur->childrens)[ch-'a'];
    }

    count++;
    cur->n++;
}
删除

从一棵单词查找树中删去一个键,第一步是找到该键对应的节点。

  • 如果该节点含有一个非空链接,那么就不需要其他操作。
  • 如果该节点的所有链接都为空,那么需要从单词查找树中删除该节点。如果删去该节点使该节点的父节点的链接也全部为空且该父节点不是单词,那么也需要删除它的父节点,并以此类推。
Trie树_第4张图片
Trie删除.png
void Trie::remove(const std::string &word) {
    if (word.empty())
        return;
    root = remove(root, word, 0);
}

typename Trie::TrieNodePtr Trie::remove(TrieNodePtr x, const std::string& word, size_t len){
    if (x == nullptr)
        return nullptr;
    if (len == word.size()){
        if (x->n > 0) {
            x->n = 0;
            count--;
        }
    } else
        (x->childrens)[word[len]-'a'] = remove((x->childrens)[word[len]-'a'], word, len+1);

    if (x->n > 0)
        return x;

    for(int i = 0; i != ALPHABET; i++)
        if ((x->childrens)[i] != nullptr)
            return x;

    delete x;
    return nullptr;
}
遍历
Trie树_第5张图片
Trie遍历.png
void Trie::collect(TrieNodePtr cur, std::string& tmp, std::vector &result){
    if (cur->n > 0)
        result.push_back(tmp);

    for (int i = 0; i != ALPHABET; i++) {
        if ((cur->childrens)[i] != nullptr) {
            tmp.append(1, static_cast('a' + i));
            collect(cur->childrens[i], tmp, result);
            tmp.erase(tmp.end()-1);
        }
    }
}

性能分析

  • 在单词查找树中查找1个word或是插入1个word时,时间复杂度为O(word.size())

  • 字母表大小为R,在一棵由N各随机键构成的的单词查找树中,未命中的查找平均所需的结点数量为~logR(N)(查找未命中的成本与键的长度无关)。

    
    
  • 一棵单词查找树中的链接总数在RN到RNw之间,w为单词的平均长度。

    
    

应用

  • 有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
  • 1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。请怎么设计和实现?
  • 一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。
  • 寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。
    (1) 请描述你解决这个问题的思路;
    (2) 请给出主要的处理流程,算法,以及算法的复杂度。

你可能感兴趣的:(Trie树)