詳解字典樹(Trie)
本篇隨筆簡單講解一下信息學奧林匹克競賽中的較為常用的數據結構——字典樹。字典樹也叫Trie樹、前綴樹。顧名思義,它是一種針對字符串進行維護的數據結構。並且,它的用途超級廣泛。建議大家熟練掌握。
字典樹的概念
字典樹,顧名思義,是關於“字典”的一棵樹。即:它是對於字典的一種存儲方式(所以是一種數據結構而不是算法)。這個詞典中的每個“單詞”就是從根節點出發一直到某一個目標節點的路徑,路徑中每條邊的字母連起來就是一個單詞。
上圖理解:
(標橙色的節點是“目標節點“,即根節點到這個目標節點的路徑上的所有字母構成了一個單詞。)
從這張圖我們可以看出,字典樹就是一棵樹(emm...有些廢話的嫌疑),只不過,這棵樹的每條邊上都有一個字母,然后這棵樹的一些節點被指定成了標記節點(目標節點)而已。
這就是字典樹的概念。結合上面說的概念,上圖所示的字典樹包括的單詞分別為:
a
abc
bac
bbc
ca
字典樹的功能
根據字典樹的概念,我們可以發現:字典樹的本質是把很多字符串拆成單個字符的形式,以樹的方式存儲起來。所以我們說字典樹維護的是”字典“。那么根據這個最基本的性質,我們可以由此延伸出字典樹的很多妙用。簡單總結起來大體如下:
-
1、維護字符串集合(即字典)。
-
2、向字符串集合中插入字符串(即建樹)。
-
3、查詢字符串集合中是否有某個字符串(即查詢)。
-
4、統計字符串在集合中出現的個數(即統計)。
-
5、將字符串集合按字典序排序(即字典序排序)。
-
6、求集合內兩個字符串的LCP(Longest Common Prefix,最長公共前綴)(即求最長公共前綴)。
我們可以發現,以上列舉出的功能都是建立在“字符串集合”的基礎上的。再一次強調,字典樹是“字典”的樹,一切功能都是“字典”的功能。這也為我們使用字典樹的時候提供了一個准則:看到一大堆字符串同時出現,就往哈希和Trie樹那邊想一下。
字典樹的實現及代碼實現
把上面的圖搬下來...
字典樹的兩種基本操作分別是建樹和查詢。其中建樹操作就是把一個新單詞插入到字典樹里。查詢操作就是查詢給定單詞是否在字典樹上。
那我們來想一下。
插入操作
假如這個字典只包括26個英文字母(暫且都定為小寫),那么這個樹的深度會由具體單詞不一樣而定。但是它的廣度范圍是可以提前確定好的。對於每個節點,廣度最大為26。(因為每個節點的下一個字母(即后綴點)只可能是26個字母。)那么我們可以用結構體開好這個“虛擬全樹”(這個名字是筆者自己起的,請大家好好理解)。然后通過深度迭代向里面嘗試加入單詞。
我們開一個包含\(26\)個后綴指針的結構體。用變量\(now\)來表示指向當前節點編號的一個指針,用\(tot\)變量表示點的編號。\(end\)數組表示當前單詞的“目標節點”即單詞結尾的那個節點具體是哪個單詞的詞尾。
那么代碼就長成這樣:
struct node
{
int nxt[27];
}trie[maxn];
int insert(char s[],int id)
{
int now=0;
int len=strlen(s);
for(int i=0;i<len;i++)
{
int ch=s[i]-'a'+1;
if(!trie[now].nxt[ch])
{
trie[now].nxt[ch]=tot;
tot++;
}
now=trie[now].nxt[ch];
}
end[now]=id;
}
查詢操作
查詢操作和剛剛的思路大同小異,因為我們已經有了一個“虛擬全樹”,那么我們還是按深度向下迭代,對於需要查詢的字符串的當前字符,如果這個對應的字符指針為空,就說明不含這個單詞,直接跳出即可。當我們都迭代完成之后,直接返回\(end[now]\)即可。(注意,這里不能直接返回\(1\)或\(true\),假如字典中只保存了一個字符串\(abcdef\),而我們查詢的是\(abc\),它可以不被跳出地一直迭代到最后,但是它並不是字典中的單詞。即,需要考慮字典中單詞子串的情況)。
代碼:
bool search(char s[])
{
int len=strlen(s);
int now=0;
for(int i=0;i<len;i++)
{
int ch=s[i]-'a'+1;
if(!trie[now].nxt[ch])
return 0;
}
return end[now];
}