TrieTree字典樹資料結構的原理、實現及應用
一、基本知識
字典樹(TrieTree),又稱單詞查詢樹或鍵樹,是一種樹形結構,是一種雜湊樹的變種。典型應用是用於統計和排序大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。它的優點是:最大限度地減少無謂的字串比較,查詢效率比雜湊表高。
Trie的核心思想是空間換時間。利用字串的公共字首來降低查詢時間的開銷以達到提高效率的目的。
它有3個基本性質:
根節點不包含字元,除根節點外每一個節點都只包含一個字元。
從根節點到某一節點,路徑上經過的字元連線起來,為該節點對應的字串。
每個節點的所有子節點包含的字元都不相同。
二、構建TrieTree
給定多個字串,如 {banana,band,apple,apt,bbc,app,ba},那麼所構建的一棵TrieTree形狀如下:
其中,黃色的節點代表從根節點通往該節點的路徑上所經過的節點的字元構成的一個字串出現在原來的輸入文字中,如以d為例,路徑上的字元為:b-a-n-d,對應輸入的字串集合中的”band”。TrieTree可以很方便的擴充套件,當來了新的字串時,只要把新的字串按照原本的規則插入到原來的樹中,便可以得到新的樹。如需要加入新的單詞”bat”,那麼樹的結構只需簡單的拓展成如下的形式:
可以看出,TrieTree充分利用字串與字串間擁有公共字首的特性,而這種特性在字串的檢索與詞頻統計中會發揮重要的作用。
三、利用TrieTree進行字串檢索
利用上一節中構造的TrieTree,我們可以很方便的檢索一個單詞是否出現在原來的字串集合中。例如,我們檢索單詞”banana”,那麼我們從根節點開始,逐層深入,由路徑b-a-n-a-n-a最終到達節點a,可以看出此時的節點a是黃色的,意味著“從根節點到該節點的路徑形成的字串出現在原來的字串集合中”,因此單詞”banana”的檢索是成功的。又如,檢索單詞”application”,從根節點沿路徑a-p-p,到達節點p後,由於節點p的後代並沒有’l’,這也意味著檢索失敗。再舉一個例子,檢索單詞”ban”,沿著路徑b-a-n到達節點n,然而,當前的節點n並不是黃色的,說明了“從根節點到該節點的路徑形成的字串“ban”沒有出現在原來的字串集合中,但該字串是原字串集合中某個(些)單詞的字首”。
可以看出,利用TrieTree進行文字串的單詞統計十分方便,當我們要檢索一個單詞的詞頻時,不用再去遍歷原來的文字串,從而實現高效的檢索。這在搜尋引擎中統計高頻的詞彙是十分有效的。
四、TrieTree的程式碼實現
以下為以C++語言實現的TrieTree資料結構。
#include<vector>
#include<string>
#include<cassert>
#include<fstream>
#include<algorithm>
#include<stack>
#include<map>
using namespace std;
#define MAX_SIZE 26 //字符集的大小,這裡預設字元都是小寫英文字母
struct TrieTreeNode{
int WordCount; //用於記錄以該節點結尾的單詞出現的次數
int PrefixCount; //用於記錄以該節點結尾的前綴出現的次數
char ch; //該節點的字元值
TrieTreeNode* parent; //指向的父節點指標,一般來說不需要,但為了後面高效的遍歷樹並統計詞頻所增加的
TrieTreeNode** child; //指向孩子的指標陣列
TrieTreeNode(){
WordCount = 0;
PrefixCount = 0;
child = new TrieTreeNode*[MAX_SIZE];
parent = NULL;
for (int i = 0; i < MAX_SIZE; ++i)
child[i] = NULL;
}
};
class TrieTree{
private:
TrieTreeNode* _root;
public:
//建構函式
TrieTree(){ _root = new TrieTreeNode();
}
//向樹插入新單詞
void insert(const string& word){
if (word.length() == 0) { return; }
insert_I(_root, word);
}
//給定某個單詞,返回其在文字中出現的次數
int findCount(const string& word){
if (word.length() == 0){ return -1; }
return findCount_I(_root, word);
}
//給定某個字首,返回其在文字中出現的次數
int findPrefix(const string& prefix){
if (prefix.length() == 0) return -1;
return findPrefix_I(_root, prefix);
}
//統計文字中出現的所有單詞及出現的次數
map<string, int> WordFrequency(){
map<string, int> tank;
countFrequency(_root, tank);
return tank;
}
private:
pair<string, int> getWordCountAtNode(TrieTreeNode* p){
int count = p->WordCount;
string word;
stack<char> S;
do{
S.push(p->ch);
p = p->parent;
} while (p->parent);
while (!S.empty()){
word.push_back(S.top());
S.pop();
}
return {word,count};
}
void countFrequency(TrieTreeNode* p,map<string, int>& tank){
if (p == NULL) return;
if (p->WordCount > 0) tank.insert(getWordCountAtNode(p));
for (int i = 0; i < MAX_SIZE; ++i){
countFrequency(p->child[i], tank);
}
}
void insert_I(TrieTreeNode* p, const string& word){
for (int i = 0; i < word.length(); ++i){
int pos = word[i] - 'a';
if (p->child[pos] == NULL){
p->child[pos] = new TrieTreeNode();
p->child[pos]->ch = word[i];
p->child[pos]->parent = p;
}
p->child[pos]->PrefixCount++;
p = p->child[pos];
}
p->WordCount++;
}
int findCount_I(TrieTreeNode* p, const string& word){
for (int i = 0; i < word.length(); ++i){
int pos = word[i] - 'a';
if (p->child[pos] == NULL) return 0;
p = p->child[pos];
}
return p->WordCount;
}
int findPrefix_I(TrieTreeNode* p, const string& prefix){
for (int i = 0; i < prefix.length(); ++i){
int pos = prefix[i] - 'a';
if (p->child[pos] == NULL) return 0;
p = p->child[pos];
}
return p->PrefixCount;
}
};
四、TrieTree的應用
利用TrieTree檢索單詞是否出現在文字中:
例如,有一文字內容如下:
the apple apple banana potato potato. potato apple
oppo potato, apple tomato the.
定義一個類FileReader用來讀取文字檔案:
class FileReader{
private:
vector<string> text;
string erase;
bool erase_flag;
public:
FileReader():erase_flag(false){}
void read(const string& filename){
ifstream infile;
infile.open(filename.data());
assert(infile.is_open());
string word;
while (infile>>word){
if (erase_flag){
for (int i = 0; i < erase.length(); ++i){
int n = 0,pos=0;
while (n<word.length())
{
pos=word.find(erase[i], pos);
if (pos < 0) break;
else{
word.erase(pos, 1);
}
}
}
text.push_back(word);
}
}
}
void InputEraseChar(const string& CharSet){
if (CharSet.length() == 0) return;
erase = CharSet;
}
void OnEraseChar(){ erase_flag = true; }
void OffEraseChar(){ erase_flag = false; }
void clearData(){
text.clear();
}
void writeData(){
if (text.size() == 0) { printf("No Data!!"); return; }
for (int i = 0; i < text.size(); ++i){
printf("%s ", text[i].data());
}
}
vector<string> outputData(){
if (text.size() == 0) return{};
vector<string> output(text);
return output;
}
private:
bool IsEraseChar(const char &ch){
return erase.find(ch);
}
};
測試1:統計某些單詞或前綴出現次數
主函式入口如下:
int main(){
FileReader reader;
reader.InputEraseChar(",.");
reader.OnEraseChar();
reader.read("F:\\TrieTreeTest.txt");
vector<string> words = reader.outputData();
TrieTree tree;
for (int i = 0; i < words.size(); ++i)
tree.insert(words[i]);
int count=tree.findCount("banana");
printf("banana: %d\n",count);
count=tree.findCount("apple");
printf("apple: %d\n",count);
int prefixCount=tree.find("ba");
printf("prefix \"ba\": %d\n",prefixCount);
return 0;
}
程式輸出如下:
測試2:統計所有出現過的單詞詞頻
int main(){
FileReader reader;
reader.InputEraseChar(",.");
reader.OnEraseChar();
reader.read("F:\\TrieTreeTest.txt");
vector<string> words = reader.outputData();
TrieTree tree;
for (int i = 0; i < words.size(); ++i)
tree.insert(words[i]);
map<string, int> tank = tree.WordFrequency();
map<string, int>::iterator begin = tank.begin();
for (; begin != tank.end(); ++begin){
printf("%s: %d\n", begin->first.c_str(), begin->second);
}
return 0;
}
程式碼結果如下:
以上就是TrieTree結構在文字中統計單詞詞頻的應用。