天天看點

Trie樹:應用于統計和排序Trie樹:應用于統計和排序

轉自:https://blog.csdn.net/hguisu/article/details/8131559

Trie樹:應用于統計和排序

 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

Trie樹:應用于統計和排序Trie樹:應用于統計和排序

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

Trie樹:應用于統計和排序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等其他符合題目要求的變量。  

Trie樹:應用于統計和排序Trie樹:應用于統計和排序

代碼:

  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. #pragma once

  6. #include <stdio.h>

  7. #include "stdlib.h"

  8. #include <iostream>

  9. #include <string.h>

  10. using namespace std;

  11. //宏定義

  12. #define TRUE 1

  13. #define FALSE 0

  14. #define NULL 0

  15. #define OK 1

  16. #define ERROR 0

  17. #define INFEASIBLE -1

  18. #define OVERFLOW -2

  19. const int num_chars = 26;

  20. class Trie {

  21. public:

  22. Trie();

  23. Trie(Trie& tr);

  24. virtual ~Trie();

  25. int trie_search(const char* word, char* entry ) const;

  26. int insert(const char* word, const char* entry);

  27. int remove(const char* word, char* entry);

  28. protected:

  29. struct Trie_node{

  30. char* data; //若不為空,表示從root到此結點構成一個單詞

  31. Trie_node* branch[num_chars]; //分支

  32. Trie_node(); //構造函數

  33. };

  34. Trie_node* root; //根結點(指針)

  35. };

  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. Trie_node *location = root; //從根結點開始

  15. while( location!=NULL && *word!=0 ) {

  16. if (*word >= 'A' && *word <= 'Z')

  17. char_code = *word-'A';

  18. else if (*word>='a' && *word<='z')

  19. char_code = *word-'a';

  20. else return 0;// 不合法的單詞

  21. //轉入相應分支指針

  22. location = location->branch[char_code];

  23. position++;

  24. word++;

  25. }

  26. //找到,擷取資料,成功傳回

  27. if ( location != NULL && location->data != NULL ) {

  28. strcpy(entry,location->data);

  29. return 1;

  30. }

  31. else return 0;// 不合法的單詞

  32. }

  33. int Trie::insert(const char* word, const char* entry) {

  34. int result = 1, position = 0;

  35. if ( root == NULL ) root = new Trie_node; //初始插入,根結點為空

  36. char char_code;

  37. Trie_node *location = root; //從根結點開始

  38. while( location!=NULL && *word!=0 ) {

  39. if (*word>='A' && *word<='Z') char_code = *word-'A';

  40. else if (*word>='a' && *word<='z') char_code = *word-'a';

  41. else return 0;// 不合法的單詞

  42. //不存在此分支

  43. if( location->branch[char_code] == NULL )

  44. location->branch[char_code] = new Trie_node; //建立空分支

  45. //轉入分支

  46. location = location->branch[char_code];

  47. position++;word++; }

  48. if (location->data != NULL) result = 0;//欲插入的單詞已經存在

  49. else { //插入資料

  50. location->data = new char[strlen(entry)+1]; //配置設定記憶體

  51. strcpy(location->data, entry); //給data指派表明單詞存在

  52. }

  53. return result;

  54. }

  55. int main(){

  56. Trie t;

  57. char entry[100];

  58. t.insert("a", "DET");

  59. t.insert("abacus","NOUN");

  60. t.insert("abalone","NOUN");

  61. t.insert("abandon","VERB");

  62. t.insert("abandoned","ADJ");

  63. t.insert("abashed","ADJ");

  64. t.insert("abate","VERB");

  65. t.insert("this", "PRON");

  66. if (t.trie_search("this", entry))

  67. cout<<"'this' was found. pos: "<<entry<<endl;

  68. if (t.trie_search("abate", entry))

  69. cout<<"'abate' is found. pos: "<<entry<<endl;

  70. if (t.trie_search("baby", entry))

  71. cout<<"'baby' is found. pos: "<<entry<<endl;

  72. else

  73. cout<<"'baby' does not exist at all!"<<endl;

  74. }

3. 查找分析

       在trie樹中查找一個關鍵字的時間和樹中包含的結點數無關,而取決于組成關鍵字的字元數。而二叉查找樹的查找時間和樹中的結點數有關O(log2n)。

       如果要查找的關鍵字可以分解成字元序列且不是很長,利用trie樹查找速度優于二叉查找樹。如:

       若關鍵字長度最大是5,則利用trie樹,利用5次比較可以從26^5=11881376個可能的關鍵字中檢索出指定的關鍵字。而利用二叉查找樹至少要進行

Trie樹:應用于統計和排序Trie樹:應用于統計和排序

次比較。

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自動機等。

繼續閱讀