Trie樹

Trie樹,即字典樹,又稱單詞查找樹。經常應用于字符串的統計與排序,經常被搜索引擎系統用于文本詞頻統計。

核心思想是:空間換時間,利用字符串的公共前綴,來降低查詢時間的開銷以達到提高效率的目的

一般有以下3個性質:

  • 根節點不包含字符,除根節點外每一個節點都只包含一個字符。
  • 從根節點到某一節點,路勁上經過的字符連接起來,為該節點對應的字符串。
  • 每個節點的所有子節點包含的字符都不相同。
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查找.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插入.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刪除.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遍歷.png
void Trie::collect(TrieNodePtr cur, std::string& tmp, std::vector<std::string> &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<char>('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) 請給出主要的處理流程,算法,以及算法的復雜度。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容