Trie樹(字典樹):應用於統計和排序

轉載這篇關於字典樹的原因是看到騰訊面試相關的題:就是在海量數據中找出某一個數,比如2億QQ號中查找出某一個特定的QQ號。。

有人提到字典樹,我就順便了解下字典樹。



[轉自:http://blog.csdn.net/oncealong/article/details/51707256]

[轉自:http://blog.csdn.NET/hguisu/article/details/8131559]

1. 什麼是trie樹

  1.Trie樹 (特例結構樹)  

      Trie樹,又稱單詞查找樹、字典樹,是一種樹形結構,是一種哈希樹的變種,是一種用於快速檢索的多叉樹結構。典型應用是用於統計和排序大量的字符串(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計。它的優點是:最大限度地減少無謂的字符串比較,查詢效率比哈希表高。
     Trie的核心思想是空間換時間。利用字符串的公共前綴來降低查詢時間的開銷以達到提高效率的目的。
     Trie樹也有它的缺點,Trie樹的內存消耗非常大.當然,或許用左兒子右兄弟的方法建樹的話,可能會好點.

2.  三個基本特性:  

1)根節點不包含字符,除根節點外每一個節點都只包含一個字符。  
2)從根節點到某一節點,路徑上經過的字符連接起來,爲該節點對應的字符串。 
3)每個節點的所有子節點包含的字符都不相同。

3 .例子

       和二叉查找樹不同,在trie樹中,每個結點上並非存儲一個元素。
       trie樹把要查找的關鍵詞看作一個字符序列。並根據構成關鍵詞字符的先後順序構造用於檢索的樹結構。
       在trie樹上進行檢索類似於查閱英語詞典。
      一棵m度的trie樹或者爲空,或者由m棵m度的trie樹構成。

     例如,電子英文詞典,爲了方便用戶快速檢索英語單詞,可以建立一棵trie樹。例如詞典由下面的單詞成:a、b、c、aa、ab、ac、ba、ca、aba、abc、baa、bab、bac、cab、abba、baba、caba、abaca、caaba


           再舉一個例子。給出一組單詞,inn, int, at, age, adv, ant, 我們可以得到下面的Trie:

        

        可以看出:

  • 每條邊對應一個字母。
  • 每個節點對應一項前綴。葉節點對應最長前綴,即單詞本身。
  • 單詞inn與單詞int有共同的前綴“in”, 因此他們共享左邊的一條分支,root->i->in。同理,ate, age, adv, 和ant共享前綴"a",所以他們共享從根節點到節點"a"的邊。

查詢操縱非常簡單。比如要查找int,順着路徑i -> in -> int就找到了。

 2. trie樹的實現

1.插入過程

對於一個單詞,從根開始,沿着單詞的各個字母所對應的樹中的節點分支向下走,直到單詞遍歷完,將最後的節點標記爲紅色,表示該單詞已插入trie樹。

2. 查找過程

其方法爲:

(1) 從根結點開始一次搜索;

(2) 取得要查找關鍵詞的第一個字母,並根據該字母選擇對應的子樹並轉到該子樹繼續進行檢索;

(3) 在相應的子樹上,取得要查找關鍵詞的第二個字母,並進一步選擇對應的子樹進行檢索。
(4) 迭代過程……
(5) 在某個結點處,關鍵詞的所有字母已被取出,則讀取附在該結點上的信息,即完成查找。其他操作類似處理.

       即從根開始按照單詞的字母順序向下遍歷trie樹,一旦發現某個節點標記不存在或者單詞遍歷完成而最後的節點未標記爲紅色,則表示該單詞不存在,若最後的節點標記爲紅色,表示該單詞存在。如下圖中:trie樹中存在的就是abc、d、da、dda四個單詞。在實際的問題中可以將標記顏色的標誌位改爲數量count等其他符合題目要求的變量。  

      


代碼:
[cpp] view plain copy
 
 print?
  1. // stdafx.h : include file for standard system include files,  
  2. // or project specific include files that are used frequently, but  
  3. // are changed infrequently  
  4. //  
  5.   
  6. #pragma once  
  7.   
  8. #include <stdio.h>    
  9. #include "stdlib.h"  
  10. #include <iostream>  
  11. #include <string.h>  
  12. using namespace std;  
  13.   
  14. //宏定義      
  15. #define TRUE   1      
  16. #define FALSE   0     
  17. #define NULL 0  
  18. #define OK    1      
  19. #define ERROR   0    
  20. #define INFEASIBLE -1      
  21. #define OVERFLOW -2    
  22.   
  23. const int num_chars = 26;  
  24. class Trie {  
  25. public:  
  26.     Trie();  
  27.     Trie(Trie& tr);  
  28.     virtual ~Trie();  
  29.     int trie_search(const char* word, char* entry ) const;  
  30.     int insert(const char* word, const char* entry);  
  31.     int remove(const char* word, char* entry);  
  32. protected:  
  33.      struct Trie_node{  
  34.            char* data; //若不爲空,表示從root到此結點構成一個單詞   
  35.            Trie_node* branch[num_chars]; //分支   
  36.            Trie_node(); //構造函數   
  37.      };  
  38.        
  39.      Trie_node* root; //根結點(指針)   
  40.   
  41. };  

[cpp] view plain copy
 
 print?
  1. // Test.cpp : Defines the entry point for the console application.    
  2. //    
  3. #include "stdafx.h"   
  4. Trie::Trie_node::Trie_node() {  
  5.     data = NULL;      
  6.     for (int i=0; i<num_chars; ++i)   
  7.         branch[i] = NULL;  
  8. }  
  9. Trie::Trie():root(NULL) {}  
  10. Trie::~Trie(){}  
  11. int Trie::trie_search(const char* word, char* entry ) const {    
  12.     int position = 0;  //層數   
  13.     char char_code;      
  14.   
  15.     Trie_node *location = root;  //從根結點開始   
  16.     while( location!=NULL && *word!=0 ) {       
  17.         if (*word >= 'A' && *word <= 'Z')   
  18.             char_code = *word-'A';       
  19.         else if (*word>='a' && *word<='z')   
  20.             char_code = *word-'a';       
  21.         else return 0;// 不合法的單詞     
  22.         //轉入相應分支指針   
  23.         location = location->branch[char_code];       
  24.         position++;       
  25.         word++;    
  26.     }    
  27.     //找到,獲取數據,成功返回   
  28.     if ( location != NULL && location->data != NULL ) {       
  29.         strcpy(entry,location->data);       
  30.         return 1;    
  31.     }    
  32.     else  return 0;// 不合法的單詞  
  33. }  
  34. int Trie::insert(const char* word, const char* entry) {     
  35.     int result = 1, position = 0;     
  36.     if ( root == NULL ) root = new Trie_node;   //初始插入,根結點爲空   
  37.     char char_code;     
  38.     Trie_node *location = root;   //從根結點開始   
  39.     while( location!=NULL && *word!=0 ) {         
  40.         if (*word>='A' && *word<='Z') char_code = *word-'A';         
  41.         else if (*word>='a' && *word<='z') char_code = *word-'a';         
  42.         else return 0;// 不合法的單詞      
  43.   
  44.         //不存在此分支   
  45.         if( location->branch[char_code] == NULL )              
  46.             location->branch[char_code] = new Trie_node;    //創建空分支     
  47.   
  48.         //轉入分支   
  49.         location = location->branch[char_code];         
  50.         position++;word++;   }     
  51.     if (location->data != NULL) result = 0;//欲插入的單詞已經存在      
  52.     else {    //插入數據       
  53.         location->data = new char[strlen(entry)+1];     //分配內存      
  54.         strcpy(location->data, entry);    //給data賦值表明單詞存在   
  55.     }     
  56.     return result;    
  57. }  
  58. int main(){     
  59.     Trie t;     
  60.     char entry[100];     
  61.     t.insert("a""DET");          
  62.     t.insert("abacus","NOUN");     
  63.     t.insert("abalone","NOUN");     
  64.     t.insert("abandon","VERB");     
  65.     t.insert("abandoned","ADJ");    
  66.     t.insert("abashed","ADJ");     
  67.     t.insert("abate","VERB");      
  68.     t.insert("this""PRON");     
  69.     if (t.trie_search("this", entry))        
  70.         cout<<"'this' was found. pos: "<<entry<<endl;     
  71.     if (t.trie_search("abate", entry))        
  72.         cout<<"'abate' is found. pos: "<<entry<<endl;     
  73.     if (t.trie_search("baby", entry))        
  74.         cout<<"'baby' is found. pos: "<<entry<<endl;     
  75.     else        
  76.         cout<<"'baby' does not exist at all!"<<endl;  
  77. }  


3. 查找分析

       在trie樹中查找一個關鍵字的時間和樹中包含的結點數無關,而取決於組成關鍵字的字符數。而二叉查找樹的查找時間和樹中的結點數有關O(log2n)。
       如果要查找的關鍵字可以分解成字符序列且不是很長,利用trie樹查找速度優於二叉查找樹。如:
       若關鍵字長度最大是5,則利用trie樹,利用5次比較可以從26^5=11881376個可能的關鍵字中檢索出指定的關鍵字。而利用二叉查找樹至少要進行次比較。

3. trie樹的應用:

1. 字符串檢索,詞頻統計,搜索引擎的熱門查詢

        事先將已知的一些字符串(字典)的有關信息保存到trie樹裏,查找另外一些未知字符串是否出現過或者出現頻率。

        舉例:

       1)有一個1G大小的一個文件,裏面每一行是一個詞,詞的大小不超過16字節,內存限制大小是1M。返回頻數最高的100個詞。

       2)給出N 個單詞組成的熟詞表,以及一篇全用小寫英文書寫的文章,請你按最早出現的順序寫出所有不在熟詞表中的生詞。

       3)給出一個詞典,其中的單詞爲不良單詞。單詞均爲小寫字母。再給出一段文本,文本的每一行也由小寫字母構成。判斷文本中是否含有任何不良單詞。例如,若rob是不良單詞,那麼文本problem含有不良單詞。

       4)1000萬字符串,其中有些是重複的,需要把重複的全部去掉,保留沒有重複的字符串

       5)尋找熱門查詢搜索引擎會通過日誌文件把用戶每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度爲1-255字節。假設目前有一千萬個記錄,這些查詢串的重複讀比較高,雖然總數是1千萬,但是如果去除重複和,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的用戶越多,也就越熱門。請你統計最熱門的10個查詢串,要求使用的內存不能超過1G。

2. 字符串最長公共前綴

       Trie樹利用多個字符串的公共前綴來節省存儲空間,反之,當我們把大量字符串存儲到一棵trie樹上時,我們可以快速得到某些字符串的公共前綴。舉例:

      1) 給出N 個小寫英文字母串,以及Q 個詢問,即詢問某兩個串的最長公共前綴的長度是多少.  解決方案:

        首先對所有的串建立其對應的字母樹。此時發現,對於兩個串的最長公共前綴的長度即它們所在結點的公共祖先個數,於是,問題就轉化爲了離線  (Offline)的最近公共祖先(Least Common Ancestor,簡稱LCA)問題。

       而最近公共祖先問題同樣是一個經典問題,可以用下面幾種方法:

        1. 利用並查集(Disjoint Set),可以採用採用經典的Tarjan 算法

        2. 求出字母樹的歐拉序列(Euler Sequence )後,就可以轉爲經典的最小值查詢(Range Minimum Query,簡稱RMQ)問題了;


3.  排序

       Trie樹是一棵多叉樹,只要先序遍歷整棵樹,輸出相應的字符串便是按字典序排序的結果。

        舉例: 給你N 個互不相同的僅由一個單詞構成的英文名,讓你將它們按字典序從小到大排序輸出。

4 作爲其他數據結構和算法的輔助結構

       如後綴樹,AC自動機等。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章