字典數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樹的結點信息的結構體:
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. typedef struct 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
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. #include<iostream>  
  2. #include<cstring>  
  3. using namespace std;  
  4.   
  5. typedef struct Trie_node  
  6. {  
  7.     int count;                    // 統計單詞前綴出現的次數  
  8.     struct Trie_node* next[26];   // 指向各個子樹的指針  
  9.     bool exist;                   // 標記該結點處是否構成單詞    
  10. }TrieNode , *Trie;  
  11.   
  12. TrieNode* createTrieNode()  
  13. {  
  14.     TrieNode* node = (TrieNode *)malloc(sizeof(TrieNode));  
  15.     node->count = 0;  
  16.     node->exist = false;  
  17.     memset(node->next , 0 , sizeof(node->next));    // 初始化爲空指針  
  18.     return node;  
  19. }  
  20.   
  21. void Trie_insert(Trie root, char* word)  
  22. {  
  23.     Trie node = root;  
  24.     char *p = word;  
  25.     int id;  
  26.     while( *p )  
  27.     {  
  28.         id = *p - 'a';  
  29.         if(node->next[id] == NULL)  
  30.         {  
  31.             node->next[id] = createTrieNode();  
  32.         }  
  33.         node = node->next[id];  // 每插入一步,相當於有一個新串經過,指針向下移動  
  34.         ++p;  
  35.         node->count += 1;      // 這行代碼用於統計每個單詞前綴出現的次數(也包括統計每個單詞出現的次數)  
  36.     }  
  37.     node->exist = true;        // 單詞結束的地方標記此處可以構成一個單詞  
  38. }  
  39.   
  40. int Trie_search(Trie root, char* word)  
  41. {  
  42.     Trie node = root;  
  43.     char *p = word;  
  44.     int id;  
  45.     while( *p )  
  46.     {  
  47.         id = *p - 'a';  
  48.         node = node->next[id];  
  49.         ++p;  
  50.         if(node == NULL)  
  51.             return 0;  
  52.     }  
  53.     return node->count;  
  54. }  
  55.   
  56. int main(void)  
  57. {  
  58.     Trie root = createTrieNode();     // 初始化字典樹的根節點  
  59.     char str[12] ;  
  60.     bool flag = false;  
  61.     while(gets(str))  
  62.     {  
  63.         if(flag)  
  64.             printf("%d\n",Trie_search(root , str));  
  65.         else  
  66.         {  
  67.             if(strlen(str) != 0)  
  68.             {  
  69.                 Trie_insert(root , str);  
  70.             }  
  71.             else  
  72.                 flag = true;  
  73.         }  
  74.     }  
  75.   
  76.     return 0;  
  77. }  
字典樹的查找
http://acm.hdu.edu.cn/showproblem.php?pid=1075

[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. #include<iostream>  
  2. #include<cstring>  
  3. using namespace std;  
  4.   
  5. typedef struct Trie_node  
  6. {  
  7.     int count;                    // 統計單詞前綴出現的次數  
  8.     struct Trie_node* next[26];   // 指向各個子樹的指針  
  9.     bool exist;                   // 標記該結點處是否構成單詞    
  10.     char trans[11];               // 翻譯  
  11. }TrieNode , *Trie;  
  12.   
  13. TrieNode* createTrieNode()  
  14. {  
  15.     TrieNode* node = (TrieNode *)malloc(sizeof(TrieNode));  
  16.     node->count = 0;  
  17.     node->exist = false;  
  18.     memset(node->next , 0 , sizeof(node->next));    // 初始化爲空指針  
  19.     return node;  
  20. }  
  21.   
  22. void Trie_insert(Trie root, char* word , char* trans)  
  23. {  
  24.     Trie node = root;  
  25.     char *p = word;  
  26.     int id;  
  27.     while( *p )  
  28.     {  
  29.         id = *p - 'a';  
  30.         if(node->next[id] == NULL)  
  31.         {  
  32.             node->next[id] = createTrieNode();  
  33.         }  
  34.         node = node->next[id];  // 每插入一步,相當於有一個新串經過,指針向下移動  
  35.         ++p;  
  36.         node->count += 1;      // 這行代碼用於統計每個單詞前綴出現的次數(也包括統計每個單詞出現的次數)  
  37.     }  
  38.     node->exist = true;        // 單詞結束的地方標記此處可以構成一個單詞  
  39.     strcpy(node->trans , trans);  
  40. }  
  41.   
  42. char* Trie_search(Trie root, char* word)  
  43. {  
  44.     Trie node = root;  
  45.     char *p = word;  
  46.     int id;  
  47.     while( *p )  
  48.     {  
  49.         id = *p - 'a';  
  50.         node = node->next[id];  
  51.         ++p;  
  52.         if(node == NULL)  
  53.             return 0;  
  54.     }  
  55.     if(node->exist)          // 查找成功  
  56.         return node->trans;  
  57.     else                     // 查找失敗  
  58.         return NULL;  
  59. }  
  60.   
  61. int main(void)  
  62. {  
  63.     Trie root = createTrieNode();     // 初始化字典樹的根節點  
  64.     char str1[3003] , str2[3003] , str[3003] , *p;  
  65.     int i , k;  
  66.   
  67.     scanf("%s",str1);  
  68.     while(scanf("%s",str1) && strcmp(str1 , "END") != 0)  
  69.     {  
  70.         scanf("%s",str2);  
  71.         Trie_insert(root , str2 , str1);  
  72.     }  
  73.   
  74.     getchar();  
  75.     gets(str1);  
  76.     k = 0;  
  77.     while(gets(str1))  
  78.     {  
  79.         if(strcmp(str1 , "END") == 0)  
  80.             break;  
  81.         for(i = 0 ; str1[i] != '\0' ; ++i)  
  82.         {  
  83.             if(str1[i] >= 'a' && str1[i] <= 'z')  
  84.             {  
  85.                 str[k++] = str1[i];  
  86.             }  
  87.             else  
  88.             {  
  89.                 str[k] = '\0';  
  90.                 p = Trie_search(root , str);  
  91.                 if(p)  
  92.                     printf("%s", p);  
  93.                 else  
  94.                     printf("%s", str);  
  95.                 k = 0;  
  96.                 printf("%c", str1[i]);  
  97.             }  
  98.         }  
  99.         printf("\n");  
  100.     }  
  101.   
  102.     return 0;  
  103. }  
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章