NOIP--資料結構--Trie樹
1、 概述
Trie樹,又稱字典樹,單詞查詢樹或者字首樹,是一種用於快速檢索的多叉樹結構,如英文字母的字典樹是一個26叉樹,數字的字典樹是一個10叉樹。
Trie一詞來自retrieve,發音為/tri:/ “tree”,也有人讀為/traɪ/ “try”。
Trie樹可以利用字串的公共字首來節約儲存空間。如下圖所示,該trie樹用10個節點儲存了6個字串tea,ten,to,in,inn,int:
在該trie樹中,字串in,inn和int的公共字首是“in”,因此可以只儲存一份“in”以節省空間。當然,如果系統中存在大量字串且這些字串基本沒有公共字首,則相應的trie樹將非常消耗記憶體,這也是trie樹的一個缺點。
Trie樹的基本性質可以歸納為:
(1)根節點不包含字元,除根節點意外每個節點只包含一個字元。
(2)從根節點到某一個節點,路徑上經過的字元連線起來,為該節點對應的字串。
(3)每個節點的所有子節點包含的字串不相同。
2、 Trie樹的基本實現
字母樹的插入(Insert)、刪除( Delete)和查詢(Find)都非常簡單,用一個一重迴圈即可,即第i 次迴圈找到前i 個字母所對應的子樹,然後進行相應的操作。實現這棵字母樹,我們用最常見的陣列儲存(靜態開闢記憶體)即可,當然也可以開動態的指標型別(動態開闢記憶體)。至於結點對兒子的指向,一般有三種方法:
1、對每個結點開一個字母集大小的陣列,對應的下標是兒子所表示的字母,內容則是這個兒子對應在大陣列上的位置,即標號;
2、對每個結點掛一個連結串列,按一定順序記錄每個兒子是誰;
3、使用左兒子右兄弟表示法記錄這棵樹。
三種方法,各有特點。第一種易實現,但實際的空間要求較大;第二種,較易實現,空間要求相對較小,但比較費時;第三種,空間要求最小,但相對費時且不易寫。
下面給出動態開闢記憶體的實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#define MAX_NUM 26
enum NODE_TYPE{ //"COMPLETED" means a string is generated so far.
COMPLETED,
UNCOMPLETED
};
struct Node {
enum NODE_TYPE type;
char ch;
struct Node* child[MAX_NUM]; //26-tree->a, b ,c, .....z
};
struct Node* ROOT; //tree root
struct Node* createNewNode( char ch){
// create a new node
struct Node *new_node = ( struct Node*) malloc ( sizeof ( struct Node));
new_node->ch = ch;
new_node->type == UNCOMPLETED;
int i;
for (i = 0; i < MAX_NUM; i++)
new_node->child[i] = NULL;
return new_node;
}
void initialization() {
//intiazation: creat an empty tree, with only a ROOT
ROOT = createNewNode( ' ' );
}
int charToindex( char ch) { //a "char" maps to an index<br>
return ch - 'a' ;
}
int find( const char chars[], int len) {
struct Node* ptr = ROOT;
int i = 0;
while (i < len) {
if (ptr->child[charToindex(chars[i])] == NULL) {
break ;
}
ptr = ptr->child[charToindex(chars[i])];
i++;
}
return (i == len) && (ptr->type == COMPLETED);
}
void insert( const char chars[], int len) {
struct Node* ptr = ROOT;
int i;
for (i = 0; i < len; i++) {
if (ptr->child[charToindex(chars[i])] == NULL) {
ptr->child[charToindex(chars[i])] = createNewNode(chars[i]);
}
ptr = ptr->child[charToindex(chars[i])];
}
ptr->type = COMPLETED;
}
|
3、 Trie樹的高階實現
可以採用雙陣列(Double-Array)實現。利用雙陣列可以大大減小記憶體使用量,具體實現細節見參考資料(5)(6)。
4、 Trie樹的應用
Trie是一種非常簡單高效的資料結構,但有大量的應用例項。
(1) 字串檢索
事先將已知的一些字串(字典)的有關資訊儲存到trie樹裡,查詢另外一些未知字串是否出現過或者出現頻率。
舉例:
@ 給出N 個單片語成的熟詞表,以及一篇全用小寫英文書寫的文章,請你按最早出現的順序寫出所有不在熟詞表中的生詞。
@ 給出一個詞典,其中的單詞為不良單詞。單詞均為小寫字母。再給出一段文字,文字的每一行也由小寫字母構成。判斷文字中是否含有任何不良單詞。例如,若rob是不良單詞,那麼文字problem含有不良單詞。
(2)字串最長公共字首
Trie樹利用多個字串的公共字首來節省儲存空間,反之,當我們把大量字串儲存到一棵trie樹上時,我們可以快速得到某些字串的公共字首。
舉例:
@ 給出N 個小寫英文字母串,以及Q 個詢問,即詢問某兩個串的最長公共字首的長度是多少?
解決方案:首先對所有的串建立其對應的字母樹。此時發現,對於兩個串的最長公共字首的長度即它們所在結點的公共祖先個數,於是,問題就轉化為了離線(Offline)的最近公共祖先(Least Common Ancestor,簡稱LCA)問題。
而最近公共祖先問題同樣是一個經典問題,可以用下面幾種方法:
1. 利用並查集(Disjoint Set),可以採用採用經典的Tarjan 演算法;
2. 求出字母樹的尤拉序列(Euler Sequence )後,就可以轉為經典的最小值查詢(Range Minimum Query,簡稱RMQ)問題了;
(關於並查集,Tarjan演算法,RMQ問題,網上有很多資料。)
(3)排序
Trie樹是一棵多叉樹,只要先序遍歷整棵樹,輸出相應的字串便是按字典序排序的結果。
舉例:
@ 給你N 個互不相同的僅由一個單詞構成的英文名,讓你將它們按字典序從小到大排序輸出。
(4) 作為其他資料結構和演算法的輔助結構
如字尾樹,AC自動機等
5、 Trie樹複雜度分析
(1) 插入、查詢的時間複雜度均為O(N),其中N為字串長度。
(2) 空間複雜度是26^n級別的,非常龐大(可採用雙陣列實現改善)。
6、 總結
Trie樹是一種非常重要的資料結構,它在資訊檢索,字串匹配等領域有廣泛的應用,同時,它也是很多演算法和複雜資料結構的基礎,如字尾樹,AC自動機等,因此,掌握Trie樹這種資料結構,對於一名IT人員,顯得非常基礎且必要!
NOIP資訊學視訊地址
視訊地址
連結:https://pan.baidu.com/s/1tHo1DFMaDuMZAemNH60dmw
提取碼:7jgr