1. 程式人生 > >[轉] Trie樹詳解及其應用

[轉] Trie樹詳解及其應用

一、知識簡介  
      最近在看字串演算法了,其中字典樹、AC自動機和字尾樹的應用是最廣泛的了,下面將會重點介紹下這幾個演算法的應用。
      字典樹(Trie)可以儲存一些字串->值的對應關係。基本上,它跟 Java 的 HashMap 功能相同,都是 key-value 對映,只不過 Trie 的 key 只能是字串。
  Trie 的強大之處就在於它的時間複雜度。它的插入和查詢時間複雜度都為 O(k) ,其中 k 為 key 的長度,與 Trie 中儲存了多少個元素無關。Hash 表號稱是 O(1) 的,但在計算 hash 的時候就肯定會是 O(k) ,而且還有碰撞之類的問題;Trie 的缺點是空間消耗很高。
  至於Trie樹的實現,可以用陣列,也可以用指標動態分配,我做題時為了方便就用了陣列,靜態分配空間。
      Trie樹,又稱單詞查詢樹或鍵樹,是一種樹形結構,是一種雜湊樹的變種。典型應用是用於統計和排序大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。它的優點是:最大限度地減少無謂的字串比較,查詢效率比雜湊表高。
      Trie的核心思想是空間換時間。利用字串的公共字首來降低查詢時間的開銷以達到提高效率的目的。

Trie樹的基本性質可以歸納為: 
(1)根節點不包含字元,除根節點意外每個節點只包含一個字元。
(2)從根節點到某一個節點,路徑上經過的字元連線起來,為該節點對應的字串。 
(3)每個節點的所有子節點包含的字串不相同。
Trie樹有一些特性:
1)根節點不包含字元,除根節點外每一個節點都只包含一個字元。
2)從根節點到某一節點,路徑上經過的字元連線起來,為該節點對應的字串。
3)每個節點的所有子節點包含的字元都不相同。
4)如果字元的種數為n,則每個結點的出度為n,這也是空間換時間的體現,浪費了很多的空間。
5)插入查詢的複雜度為O(n),n為字串長度。
基本思想(以字母樹為例):
1、插入過程
對於一個單詞,從根開始,沿著單詞的各個字母所對應的樹中的節點分支向下走,直到單詞遍歷完,將最後的節點標記為紅色,表示該單詞已插入Trie樹。
2、查詢過程
同樣的,從根開始按照單詞的字母順序向下遍歷trie樹,一旦發現某個節點標記不存在或者單詞遍歷完成而最後的節點未標記為紅色,則表示該單詞不存在,若最後的節點標記為紅色,表示該單詞存在。

二、字典樹的資料結構:

    利用串構建一個字典樹,這個字典樹儲存了串的公共字首資訊,因此可以降低查詢操作的複雜度。
    下面以英文單詞構建的字典樹為例,這棵Trie樹中每個結點包括26個孩子結點,因為總共有26個英文字母(假設單詞都是小寫字母組成)。
    則可宣告包含Trie樹的結點資訊的結構體:
  1. typedefstruct Trie_node  
  2. {  
  3.     int count;                    // 統計單詞前綴出現的次數
  4.     struct Trie_node* next[26];   // 指向各個子樹的指標
  5.     bool exist;                   
    // 標記該結點處是否構成單詞  
  6. }TrieNode , *Trie;  
      其中next是一個指標陣列,存放著指向各個孩子結點的指標。
      如給出字串"abc","ab","bd","dda",根據該字串序列構建一棵Trie樹。則構建的樹如下:

Trie樹的根結點不包含任何資訊,第一個字串為"abc",第一個字母為'a',因此根結點中陣列next下標為'a'-97的值不為NULL,其他同理,構建的Trie樹如圖所示,紅色結點表示在該處可以構成一個單詞。很顯然,如果要查詢單詞"abc"是否存在,查詢長度則為O(len),len為要查詢的字串的長度。而若採用一般的逐個匹配查詢,則查詢長度為O(len*n),n為字串的個數。顯然基於Trie樹的查詢效率要高很多。
如上圖中:Trie樹中存在的就是abc、ab、bd、dda四個單詞。在實際的問題中可以將標記顏色的標誌位改為數量count等其他符合題目要求的變數。
已知n個由小寫字母構成的平均長度為10的單詞,判斷其中是否存在某個串為另一個串的字首子串。下面對比3種方法:

1、 最容易想到的:即從字串集中從頭往後搜,看每個字串是否為字串集中某個字串的字首,複雜度為O(n^2)。

2、 使用hash:我們用hash存下所有字串的所有的字首子串。建立存有子串hash的複雜度為O(n*len)。查詢的複雜度為O(n)* O(1)= O(n)。

3、 使用Trie:因為當查詢如字串abc是否為某個字串的字首時,顯然以b、c、d....等不是以a開頭的字串就不用查找了,這樣迅速縮小查詢的範圍和提高查詢的針對性。所以建立Trie的複雜度為O(n*len),而建立+查詢在trie中是可以同時執行的,建立的過程也就可以成為查詢的過程,hash就不能實現這個功能。所以總的複雜度為O(n*len),實際查詢的複雜度只是O(len)。
三、Trie樹的操作
    在Trie樹中主要有3個操作,插入、查詢和刪除。一般情況下Trie樹中很少存在刪除單獨某個結點的情況,因此只考慮刪除整棵樹。
1、插入
  假設存在字串str,Trie樹的根結點為root。i=0,p=root。
  1)取str[i],判斷p->next[str[i]-97]是否為空,若為空,則建立結點temp,並將p->next[str[i]-97]指向temp,然後p指向temp;
   若不為空,則p=p->next[str[i]-97];
  2)i++,繼續取str[i],迴圈1)中的操作,直到遇到結束符'\0',此時將當前結點p中的 exist置為true。
2、查詢
  假設要查詢的字串為str,Trie樹的根結點為root,i=0,p=root
  1)取str[i],判斷判斷p->next[str[i]-97]是否為空,若為空,則返回false;若不為空,則p=p->next[str[i]-97],繼續取字元。
  2)重複1)中的操作直到遇到結束符'\0',若當前結點p不為空並且 exist 為true,則返回true,否則返回false。
3、刪除
  刪除可以以遞迴的形式進行刪除。
字首查詢的典型應用:
http://acm.hdu.edu.cn/showproblem.php?pid=1251
  1. #include<iostream>
  2. #include<cstring>
  3. usingnamespace std;  
  4. typedefstruct Trie_node  
  5. {  
  6.     int count;                    // 統計單詞前綴出現的次數
  7.     struct Trie_node* next[26];   // 指向各個子樹的指標
  8.     bool exist;                   // 標記該結點處是否構成單詞  
  9. }TrieNode , *Trie;  
  10. TrieNode* createTrieNode()  
  11. {  
  12.     TrieNode* node = (TrieNode *)malloc(sizeof(TrieNode));  
  13.     node->count = 0;  
  14.     node->exist = false;  
  15.     memset(node->next , 0 , sizeof(node->next));    // 初始化為空指標
  16.     return node;  
  17. }  
  18. void Trie_insert(Trie root, char* word)  
  19. {  
  20.     Trie node = root;  
  21.     char *p = word;  
  22.     int id;  
  23.     while( *p )  
  24.     {  
  25.         id = *p - 'a';  
  26.         if(node->next[id] == NULL)  
  27.         {  
  28.             node->next[id] = createTrieNode();  
  29.         }  
  30.         node = node->next[id];  // 每插入一步,相當於有一個新串經過,指標向下移動
  31.         ++p;  
  32.         node->count += 1;      // 這行程式碼用於統計每個單詞前綴出現的次數(也包括統計每個單詞出現的次數)
  33.     }  
  34.     node->exist = true;        // 單詞結束的地方標記此處可以構成一個單詞
  35. }  
  36. int Trie_search(Trie root, char* word)  
  37. {  
  38.     Trie node = root;  
  39.     char *p = word;  
  40.     int id;  
  41.     while( *p )  
  42.     {  
  43.         id = *p - 'a';  
  44.         node = node->next[id];  
  45.         ++p;  
  46.         if(node == NULL)  
  47.             return 0;  
  48.     }  
  49.     return node->count;  
  50. }  
  51. int main(void)  
  52. {  
  53.     Trie root = createTrieNode();     // 初始化字典樹的根節點
  54.     char str[12] ;  
  55.     bool flag = false;  
  56.     while(gets(str))  
  57.     {  
  58.         if(flag)  
  59.             printf("%d\n",Trie_search(root , str));  
  60.         else
  61.         {  
  62.             if(strlen(str) != 0)  
  63.             {  
  64.                 Trie_insert(root , str);  
  65.             }  
  66.             else
  67.                 flag = true;  
  68.         }  
  69.     }  
  70.     return 0;  
  71. }  
字典樹的查詢
  1. #include<iostream>
  2. #include<cstring>
  3. usingnamespace std;  
  4. typedefstruct Trie_node  
  5. {  
  6.     int count;                    // 統計單詞前綴出現的次數
  7.     struct Trie_node* next[26];   // 指向各個子樹的指標
  8.     bool exist;                   // 標記該結點處是否構成單詞  
  9.     char trans[11];               // 翻譯
  10. }TrieNode , *Trie;  
  11. TrieNode* createTrieNode()  
  12. {  
  13.     TrieNode* node = (TrieNode *)malloc(sizeof(TrieNode));  
  14.     node->count = 0;  
  15.     node->exist = false;  
  16.     memset(node->next , 0 , sizeof(node->next));    // 初始化為空指標
  17.     return node;  
  18. }  
  19. void Trie_insert(Trie root, char* word , char* trans)  
  20. {  
  21.     Trie node = root;  
  22.     char *p = word;  
  23.     int id;  
  24.     while( *p )  
  25.     {  
  26.         id = *p - 'a';  
  27.         if(node->next[id] == NULL)  
  28.         {  
  29.             node->next[id] = createTrieNode();  
  30.         }  
  31.         node = node->next[id];  // 每插入一步,相當於有一個新串經過,指標向下移動
  32.         ++p;  
  33.         node->count += 1;      // 這行程式碼用於統計每個單詞前綴出現的次數(也包括統計每個單詞出現的次數)
  34.     }  
  35.     node->exist = true;        // 單詞結束的地方標記此處可以構成一個單詞
  36.     strcpy(node->trans , trans);  
  37. }  
  38. char* Trie_search(Trie root, char* word)  
  39. {  
  40.     Trie node = root;  
  41.     char *p = word;  
  42.     int id;  
  43.     while( *p )  
  44.     {  
  45.         id = *p - 'a';  
  46.         node = node->next[id];  
  47.         ++p;  
  48.         if(node == NULL)  
  49.             return 0;  
  50.     }  
  51.     if(node->exist)          // 查詢成功
  52.         return node->trans;  
  53.     else// 查詢失敗
  54.         return NULL;  
  55. }  
  56. int main(void)  
  57. {  
  58.     Trie root = createTrieNode();     // 初始化字典樹的根節點
  59.     char str1[3003] , str2[3003] , str[3003] , *p;  
  60.     int i , k;  
  61.     scanf("%s",str1);  
  62.     while(scanf("%s",str1) && strcmp(str1 ,