字首樹及C++實現
文章
目錄
2、樹的構建與查詢
3、Trie樹的應用
4、C++實現Trie樹以及解決一些字串問題
字首樹
1 什麼是Trie樹
Trie樹,即字首樹,又稱單詞查詢樹,字典樹,是一種樹形結構,是一種雜湊樹的變種。典型應用是用於統計和排序大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。
Trie樹的核心思想是空間換時間,利用字串的公共字首來降低查詢時間的開銷以達到提高效率的目的。 它的優點是:最大限度地減少無謂的字串比較,查詢效率比雜湊表高。
它有3個基本性質:
- 根節點不包含字元,除根節點外每一個節點都只包含一個字元。
- 從根節點到某一節點,路徑上經過的字元連線起來,為該節點對應的字串。
- 每個節點的所有子節點包含的字元都不相同。
2 樹的構建與查詢
根據一個例子來描述trie樹的構建:
題目:給定100000個長度不超過10的單詞,判斷每一個單詞是否出現過,如果出現過,給出第一次出現的位置。
思路:如果對每一個單詞都遍歷整個單詞序列,那麼時間複雜度就是O ( n 2 ) O(n^2)O(n2),單詞序列的長度是100000,顯然這樣的時間複雜度代價太高了。但是考慮100000個單詞肯定有一些字串的重複,trie樹,即字首樹來解決這個問題最恰當不過了。一個字首樹的形式是什麼樣的呢?
假設有abc,abd,bcd,efg,hii,那麼我們構建如下一個樹結構用於表示這些單詞:
但是構造一個字首樹就是為了使遍歷那些有相同的字首的單詞時更快速,目前這個多叉樹上從根節點到葉節點每一條路徑都是一個單詞,如果我們增加了單詞b,abcd兩個單詞呢,還是這樣一條路走到黑,我們無法區分字母短的單詞是否已經儲存過,為了解決這個問題,我們可以在節點上記錄資訊,比如如果到達該字母后單詞結尾,我們就在時候的節點上記錄end+1,這樣就可以知道有幾個以該字首結尾的單詞,修改之後結構如下:
這是我們可以很明顯看出來如最左邊一條路徑上,有一個單詞是abc,一個單詞是abcd。根據新新增的資訊,我們可以知道所有字串中有多少個該字串,如果現在有另外一個問題,我想知道所有字串中有多少個以該字串作為字首,那是不是得遍歷該條路徑後面的節點,將所有的end加起來。為了簡單,我們仍然可以在節點中多存入一個資訊,每個節點被劃過了多少次,也就是在建立多叉樹的時候就記錄了以所有字串中有多少個以某字串作為字首,劃過的次數就是這個值。調整後結構如下:注:在C++實現部分,圖中所有的S都加1,單詞本身也是以他自己作為字首的。
回到最初的題目,這樣的結構可以根據節點的資訊得到一個單詞時候出現過(End>0),但是有一個要求我們沒有滿足,那就是如果存在這個單詞我們如何返回這個單詞在原來列表中的位置呢?根據前面介紹的多叉樹結構的介紹,我想這個問題也很容易解決,節點中我們再多存入一些資訊就可以,對於非結尾字元End=0,不需要儲存其他的資訊,對於結尾字元End>0,此時再存入資訊,標註該單詞在100000個單詞詞表中的位置,構建查詢多叉樹的時候就可以直接返回這個位置資訊了。
3 Trie樹的應用
除了本文引言處所述的問題能應用Trie樹解決之外,Trie樹還能解決下述問題(節選自此文:海量資料處理面試題集錦與Bit-map詳解,說實話,作者總結的內容挺全的,但是總感覺亂七八糟的……吐槽是不對,我反省……):
- 3、有一個1G大小的一個檔案,裡面每一行是一個詞,詞的大小不超過16位元組,記憶體限制大小是1M。返回頻數最高的100個詞。
- 9、1000萬字符串,其中有些是重複的,需要把重複的全部去掉,保留沒有重複的字串。請怎麼設計和實現?
- 10、 一個文字檔案,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間複雜度分析。
- 13、尋找熱門查詢:搜尋引擎會通過日誌檔案把使用者每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度為1-255位元組。假設目前有一千萬個記錄,這些查詢串的重複讀比較高,雖然總數是1千萬,但是如果去除重複和,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的使用者越多,也就越熱門。請你統計最熱門的10個查詢串,要求使用的記憶體不能超過1G。
(1) 請描述你解決這個問題的思路;
(2) 請給出主要的處理流程,演算法,以及演算法的複雜度。
4 C++實現Trie樹,以及解決一些字串問題
一個字串型別的陣列arr1,另一個字串型別的陣列arr2。
- arr2中有哪些字串,是arr1中出現的?請列印
- arr2中有哪些字串,是作為arr1中某個字串前綴出現的?請列印
- arr2中有哪些字串,是作為arr1中某個字串前綴出現的?請列印arr2中出現次數最大的字首。
結果如圖,具體實現在下面:
#include <iostream>
#include <string>
#include <string.h>
using namespace std;
const int MaxBranchNum = 26;//可以擴充套件
class TrieNode{
public:
string word;
int path; //該字元被劃過多少次,用以統計以該字串作為字首的字串的個數
int End; //以該字元結尾的字串
TrieNode* nexts[MaxBranchNum];
TrieNode()
{
word = "";
path = 0;
End = 0;
memset(nexts,NULL,sizeof(TrieNode*) * MaxBranchNum);
}
};
class TrieTree{
private:
TrieNode *root;
public:
TrieTree();
~TrieTree();
//插入字串str
void insert(string str);
//查詢字串str是否出現過,並返回作為字首幾次
int search(string str);
//刪除字串str
void Delete(string str);
void destory(TrieNode* root);
//列印樹中的所有節點
void printAll();
//列印以str作為字首的單詞
void printPre(string str);
//按照字典順序輸出以root為根的所有單詞
void Print(TrieNode* root);
//返回以str為字首的單詞的個數
int prefixNumbers(string str);
};
TrieTree::TrieTree()
{
root = new TrieNode();
}
TrieTree::~TrieTree()
{
destory(root);
}
void TrieTree::destory(TrieNode* root)
{
if(root == nullptr)
return ;
for(int i=0;i<MaxBranchNum;i++)
{
destory(root->nexts[i]);
}
delete root;
root = nullptr;
}
void TrieTree::insert(string str)
{
if(str == "")
return ;
char buf[str.size()];
strcpy(buf, str.c_str());
TrieNode* node = root;
int index = 0;
for(int i=0; i<strlen(buf); i++)
{
index = buf[i] - 'a';
if(node->nexts[index] == nullptr)
{
node->nexts[index] = new TrieNode();
}
node = node->nexts[index];
node->path++;//有一條路徑劃過這個節點
}
node->End++;
node->word = str;
}
int TrieTree::search(string str)
{
if(str == "")
return 0;
char buf[str.size()];
strcpy(buf, str.c_str());
TrieNode* node = root;
int index = 0;
for(int i=0;i<strlen(buf);i++)
{
index = buf[i] - 'a';
if(node->nexts[index] == nullptr)
{
return 0;
}
node = node->nexts[index];
}
if(node != nullptr)
{
return node->End;
}else
{
return 0;
}
}
void TrieTree::Delete(string str)
{
if(str == "")
return ;
char buf[str.size()];
strcpy(buf, str.c_str());
TrieNode* node = root;
TrieNode* tmp;
int index = 0;
for(int i = 0 ; i<str.size();i++)
{
index = buf[i] - 'a';
tmp = node->nexts[index];
if(--node->nexts[index]->path == 0)
{
delete node->nexts[index];
}
node = tmp;
}
node->End--;
}
int TrieTree::prefixNumbers(string str)
{
if(str == "")
return 0;
char buf[str.size()];
strcpy(buf, str.c_str());
TrieNode* node = root;
int index = 0;
for(int i=0;i<strlen(buf);i++)
{
index = buf[i] - 'a';
if(node->nexts[index] == nullptr)
{
return 0;
}
node = node->nexts[index];
}
return node->path;
}
void TrieTree::printPre(string str)
{
if(str == "")
return ;
char buf[str.size()];
strcpy(buf, str.c_str());
TrieNode* node = root;
int index = 0;
for(int i=0;i<strlen(buf);i++)
{
index = buf[i] - 'a';
if(node->nexts[index] == nullptr)
{
return ;
}
node = node->nexts[index];
}
Print(node);
}
void TrieTree::Print(TrieNode* node)
{
if(node == nullptr)
return ;
if(node->word != "")
{
cout<<node->word<<" "<<node->path<<endl;
}
for(int i = 0;i<MaxBranchNum;i++)
{
Print(node->nexts[i]);
}
}
void TrieTree::printAll()
{
Print(root);
}
int main()
{
cout << "Hello world!" << endl;
TrieTree trie;
string str = "li";
cout<<trie.search(str)<<endl;
trie.insert(str);
cout<<trie.search(str)<<endl;
trie.Delete(str);
cout<<trie.search(str)<<endl;
trie.insert(str);
cout<<trie.search(str)<<endl;
trie.insert(str);
cout<<trie.search(str)<<endl;
trie.Delete("li");
cout<<trie.search(str)<<endl;
trie.Delete("li");
cout<<trie.search(str)<<endl;
trie.insert("lia");
trie.insert("lic");
trie.insert("liab");
trie.insert("liad");
trie.Delete("lia");
cout<<trie.search("lia")<<endl;
cout<<trie.prefixNumbers("lia")<<endl;
return 0;
}