本文討論一棵最簡單的trie樹,基於英文26個字母組成的字符串,討論插入字符串、判斷前綴是否存在、查找字符串等基本操作;至於trie樹的刪除單個節點實在是少見,故在此不做詳解。
l Trie原理
Trie的核心思想是空間換時間。利用字符串的公共前綴來降低查詢時間的開銷以達到提高效率的目的。
l Trie性質
好多人說trie的根節點不包含任何字符信息,我所習慣的trie根節點卻是包含信息的,而且認爲這樣也方便,下面說一下它的性質 (基於本文所討論的簡單trie樹)
1. 字符的種數決定每個節點的出度,即branch數組(空間換時間思想)
2. branch數組的下標代表字符相對於a的相對位置
3. 採用標記的方法確定是否爲字符串。
4. 插入、查找的複雜度均爲O(len),len爲字符串長度
l Trie的示意圖
如圖所示,該trie樹存有abc、d、da、dda四個字符串,如果是字符串會在節點的尾部進行標記。沒有後續字符的branch分支指向NULL
l TrieTrie的優點舉例
已知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)。
解釋一下hash爲什麼不能將建立與查詢同時執行,例如有串:911,911456輸入,如果要同時執行建立與查詢,過程就是查詢911,沒有,然後存入9、91、911,查詢911456,沒有然後存入9114、91145、911456,而程序沒有記憶功能,並不知道911在輸入數據中出現過。所以用hash必須先存入所有子串,然後for循環查詢。
而trie樹便可以,存入911後,已經記錄911爲出現的字符串,在存入911456的過程中就能發現而輸出答案;倒過來亦可以,先存入911456,在存入911時,當指針指向最後一個1時,程序會發現這個1已經存在,說明911必定是某個字符串的前綴,該思想是我在做pku上的3630中發現的,詳見本文配套的“入門練習”
|
10.3 Trie樹
當關鍵碼是可變長時,Trie樹是一種特別有用的索引結構。
10.3.1 Trie樹的定義
Trie樹是一棵度 m ≥ 2 的樹,它的每一層分支不是靠整個關鍵碼的值來確定,而是由關鍵碼的一個分量來確定。
如下圖所示Trie樹,關鍵碼由英文字母組成。它包括兩類結點:元素結點和分支結點。元素結點包含整個key數據;分支結點有27個指針,其中有一個空白字符‘b’,用來終結關鍵碼;其它用來標識‘a’, ‘b’,..., ‘z’等26個英文字母。
在第0層,所有的關鍵碼根據它們第0位字符, 被劃分到互不相交的27個類中。
因此,root→brch.link[i] 指向一棵子Trie樹,該子Trie樹上所包含的所有關鍵碼都是以第 i 個英文字母開頭。
若某一關鍵碼第 j 位字母在英文字母表中順序爲 i ( i = 0, 1, ?, 26 ), 則它在Trie樹的第 j 層分支結點中從第 i 個指針向下找第 j+1 位字母所在結點。當一棵子Trie樹上只有一個關鍵碼時,就由一個元素結點來代替。在這個結點中包含有關鍵碼,以及其它相關的信息,如對應數據對象的存放地址等。
Trie樹的類定義:
const int MaxKeySize = 25; //關鍵碼最大位數
typedef struct { //關鍵碼類型
char ch[MaxKeySize]; //關鍵碼存放數組
int currentSize; //關鍵碼當前位數
} KeyType;
class TrieNode { //Trie樹結點類定義
friend class Trie;
protected:
enum { branch, element } NodeType; //結點類型
union NodeType { //根據結點類型的兩種結構
struct { //分支結點
int num; //本結點關鍵碼個數
TrieNode *link[27]; //指針數組
} brch;
struct { //元素結點
KeyType key; //關鍵碼
recordNode *recptr; //指向數據對象指針
} elem;
}
}
class Trie { //Trie樹的類定義
private:
TrieNode *root, *current;
public:
RecordNode* Search ( const keyType & );
int Insert ( const KeyType & );
int Delete ( const KeyType & );
}
10.3.2 Trie樹的搜索
爲了在Trie樹上進行搜索,要求把關鍵碼分解成一些字符元素, 並根據這些字符向下進行分支。
函數 Search 設定 current = NULL, 表示不指示任何一個分支結點, 如果 current 指向一個元素結點 elem,則 current→elem.key 是 current 所指結點中的關鍵碼。
Trie樹的搜索算法:
RecordNode* Trie::Search ( const KeyType & x ) {
k = x.key;
concatenate ( k, ‘b’ );
current = root;
int i = 0; //掃描初始化
while ( current != NULL && current→NodeType != element && i <= x.ch[i] ) {
current = current→brch.link[ord (x.ch[i])];
i = i++;
};
if ( current != NULL && current→NodeType == element && current→elem.key == x )
return current→recptr;
else
return NULL;
}
經驗證,Trie樹的搜索算法在最壞情況下搜索的時間代價是 O(l)。其中, l 是Trie樹的層數(包括分支結點和元素結點在內)。
在用作索引時,Trie樹的所有結點都駐留在磁盤上。搜索時最多做 l 次磁盤存取。
當結點駐留在磁盤上時,不能使用C++的指針 (pointer) 類型, 因爲C++不允許指針的輸入 / 輸出。在結點中的 link 指針可改用整型(integer) 實現。
10.3.3 在Trie樹上的插入和刪除
示例:插入關鍵碼bobwhite和bluejay。
a. 插入 x = bobwhite 時,首先搜索Trie樹尋找 bobwhite 所在的結點。
b. 如果找到結點, 並發現該結點的 link[‘o’] = NULL。x不在Trie樹中, 可以在該處插入。插入結果參看圖。
c. 插入 x = bluejay時,用Trie樹搜索算法可找到包含有 bluebird 的元素結點,關鍵碼bluebird 和 bluejay 是兩個不同的值,它們在第5個字母處不匹配。從 Trie樹沿搜索路徑,在第4層分支。插入結果參看圖。
在Trie樹上插入bobwhite和bluejay後的結果 :
示例:考慮在上圖所示Trie樹中刪除bobwhite。此時,只要將該結點link[‘o’]置爲0 (NULL)即可,Trie樹的其它部分不需要改變。
考慮刪除 bluejay。刪除之後在標記爲δ3 的子Trie樹中只剩下一個關鍵碼,這表明可以刪去結點δ3 ,同時結點 ρ 向上移動一層。對結點δ2 和δ1 可以做同樣的工作,最後到達結點б。因爲以б 爲根的子Trie樹中有多個關鍵碼,所以它不能刪去,令該結點link[‘l’] = ρ即可。
爲便於Trie樹的刪除, 在每個分支結點中設置了一個 num 數據成員,它記載了結點中子女的數目。
Trie,又稱單詞查找樹,是一種樹形結構,用於保存大量的字符串。它的優點是:利用字符串的公共前綴來節約存儲空間。
性質
它有3個基本性質:
例子
這是一個Trie結構的例子:
在這個Trie結構中,保存了t、to、te、tea、ten、i、in、inn這8個字符串,僅佔用8個字節(不包括指針佔用的空間)