首先說明一下後綴樹系列一共會有三篇文章,本文先介紹基本概念以及如何線性時間內構件後綴樹,第二篇文章會詳細介紹怎麼實現後綴樹(包含實現代碼),第三篇會着重談一談後綴樹的應用。html
本文分爲三個部分,算法
在接觸後綴樹以前先簡單聊聊trie樹,也就是字典樹。trie樹有三個性質:優化
將一系列字符串插入到trie樹的過程能夠這樣來實現:首先,樹根不存任何字符;對於每一個字符串,從左到右,沿着樹從根節點開始往下走直到找不到「路」能夠走的時候,「本身開闢一條路」繼續往下走。好比往trie樹裏面存放ana$, ann$, anna$, 以及anne$是個字符串的時候(注意一下,$是用來標誌字符串末尾),咱們會的到這樣一棵樹:見下左圖spa
上左圖這樣存儲的時候有點浪費。爲了更高效咱們把沒有分支的路徑壓縮,因而獲得上右圖。很簡單吧.net
介紹完trie樹以後呢,咱們再來看一看後綴,直接列出一個字符串MISSISSIPPI的全部後綴3d
1. MISSISSIPPI
2. ISSISSIPPI
3. SSISSIPPI
4. SISSIPPI
5. ISSIPPI
6. SSIPPI
7. SIPPI
8. IPPI
9. PPI
10. PI
11. Ihtm
而將這些後綴所有插入前面提到的trie樹中並壓縮,就獲得後綴樹啦blog
所謂的平方時間是指O(|T|*|T|),|T|是指字符串的長度。字符串
第一種方法很是顯然,就是直接按照後綴樹的定義來就能夠了,將各個後綴依次插入trie樹中,再壓縮,總的時間複雜度顯然是平方級別的。get
這裏給出的是另一種方法。對照上面MISSISSIPPI的全部後綴,咱們注意到第一種方法就是從左到右掃描完一個後綴再從上到下掃描全部的後綴。那麼另一種思路就是,先安位對齊,而後從上到下掃描完每一個位,再從左到右掃描下一位。舉個例子吧,第一種方法至關於先掃描完後綴1:MISSISSIPPI ,再往下掃描後綴2:ISSISSIPPI 以此類推;而第二種方法至關於從上到下先插入第一個字符M,而後再從上到下插入第二個字符I(有兩個),而後再從上到下插入字符S(有三個)以此類推,參見下圖。
可是具體怎麼操做呢?由於顯然每次操做不能是簡簡單單的插入字符而已!
咱們再後頭來看看上述過程,形式化一點,咱們將原先的字符串表示爲
T = t1t2 … tn$,其中ti表示第i個字符
Pi = t1t2 … ti , i:th prefix of T
那麼,咱們每次插入字符ti,至關於完成一個從Trie(Pi-1)到Trie(Pi)的過程,當全部字符插入完畢的時候咱們整個後綴樹也就構建出來了。參見下圖:插入第二個字符b至關於完成了從Trie(a)到Trie(ab)的過程。。。。
那咱們怎麼作呢?
上圖中也提示了,其實咱們須要額外保留一個尾部鏈表,鏈接着當前的「尾部」節點--也就是對應着Pi的一個後綴的那些個點。咱們注意到尾部鏈表其實是從表示T[0 .. i]後綴的點指向表示T[1 .. i]後綴的點再指向表示T[2 .. i]後綴的點,以此類推。
也能夠看得出來,每次插入一個字符都須要遍歷一下鏈表,第一次遍歷的時候鏈表長度爲1(就是根節點),第二次遍歷的時候鏈表長度爲2(點a,和根節點,參見Trie(a) ),以此類推,可知遍歷的總複雜度是O(|T|*|T|),創建鏈表也須要O(|T|*|T|),後續壓縮Trie也須要O(|T|*|T|),故而整個算法複雜度就是O(|T|*|T|)。
如今說明一下爲何算法是正確的?Trie(Pi-1)存儲的是Pi-1的全部後綴,Trie(Pi)存儲的是Pi的全部後綴。Pi的後綴能夠由Pi-1全部後綴後面插入字符ti,以及後綴ti所構成。那麼咱們沿着Trie(Pi-1)尾部鏈表插入字符ti的過程也就是插入Pi的全部後綴的過程,全部算法是正確的。
可是,有沒有小失望,畢竟幹了這麼久發現跟第一種方法相比沒有收益(哭!)。
其實不用失望,咱們作這麼多的目的在於經過改進,整個算法能夠實現線性的,下面就一步步介紹這種改進算法。
首先一點咱們必須直接在後綴樹上操做了,不能先創建Trie樹再壓縮,由於遍歷Trie樹的複雜度就已是平方級別了。
咱們定義幾種節點:
接下來咱們來看看前面提到的尾部鏈表,尾部鏈表顯然包含了當先後綴樹中的葉節點以及部分的顯式/隱式節點。沿着尾部鏈表更新:
咱們用個例子來講明一下怎麼操做,爲了便於說明隱式節點,我採用Trie樹表示:
從第三個圖到第四個圖,沿着尾部鏈表插入字符a,那麼鏈表第一個節點爲葉節點,故而直接在邊上插入這個字符就行了;鏈表第二個節點仍是葉子,在邊上插入字符就行了;第三個節點是隱式節點,看看緊跟着隱式節點後面的字符,不是a,故而將這個隱式節點變爲顯式節點,再增長一個葉子;第四個是顯式節點(根節點),其緊跟的字符集和爲{a,b},a出如今這個集合中,故而不用改變結構了。固然了,鏈表仍是要維護的啊,O(∩_∩)O哈哈~
好了,到此,咱們實現了直接在後綴樹上操做而徹底撇開Trie樹了,小有進步啦,~\(≧▽≦)/~啦啦啦
如今開始優化啦!
首先一點,在後綴樹上直接操做的時候,邊上的字符串就不必直接存儲啦,咱們能夠存這個字符串對於在原先總的字符串T中的座標。如上方右邊那個圖就是將左邊第四個圖,壓縮以後獲得的後綴樹。[2,4]就表示baa。
這樣一來啊,存儲後綴樹的空間就大大減少了。
接着,咱們來看一下啊,後綴樹S(Pi-1)中的葉子節點在S(Pi)中也是葉子節點,也就是說」一朝爲葉,終身爲葉「。並且咱們還能夠注意到尾部鏈表的前半部分全是葉子。也就是說若是S(Pi)有k個葉子,那麼表示T[0 .. i],……,T[k-1 .. i]後綴的點全是葉子。
咱們首先來看一下何時後綴會不在葉子上:T[j .. i-1]不在S(Pi-1)葉子上,代表表明該後綴的點以後還有點存在,也就是說T[0 .. i-1]中存在子串S=T[j .. i-1] + c’ ,其中c'不爲空。注意一下這是充分必要條件,由於葉子節點後面是不可能還存在點的。
如今咱們來證實一下:(ti加入到 S(Pi-1) 的過程)
咱們來利用一下上述性質。葉節點每次更新都是把ti插入到葉子所在邊的後綴字符串中,因此表示字符串的區間就變成了[ , i]。那麼咱們還有必要每次都沿着尾部鏈表去更新麼?
咱們能夠這樣,將葉子那個邊上的表示字符串的區間用[ , #]來表示,#表示當前插入字符在T中的下標。那麼這樣一來,葉子節點就自動更新啦。
再利用第二個性質,咱們徹底就能夠無論尾部鏈表的前k個節點啦。
這是又一大進步!
我們接着來!
咱們來看,根據沿尾部鏈表更新的算法,不管是顯式節點仍是隱式節點,當帶插入字符ti出如今節點的緊跟字符集合的時候,咱們就不用管了。也就是說若是T[j .. i]出如今S(Pi-1),也就是S(T[0 .. i-1]),中的時候,咱們就不用改變樹的結構了(固然須要還調整一些參數)。
咱們再來看,對於任何 j < i-1,若是T[j .. i]出如今S(T[0 .. i-1])中,那麼T[j+1 .. i]也必然出如今S(T[0 .. i-1])中。下面給出證實:
這也就是說若是尾部鏈表中某一個節點所表明的後綴加上ti,也就是T[j .. i],出如今S(T[0 .. i-1])中,那麼鏈表後面的全部節點表明的後綴加上ti也都出如今S(T[0 .. i-1])中。
故而全部這些點,不管是顯式仍是隱式節點均可以不用管了。
這又是一個大優化!
綜合上面兩個優化,咱們知道事實上咱們只須要處理原先尾部鏈表的中間一段節點就能夠了,對於這些節點而言,每處理一次一定增長一個新葉子(爲何呢,由於這些節點既不是葉子節點,又不知足顯或是隱式節點不用增長葉子的條件)。而」一朝爲葉,終身爲葉「,咱們最終的後綴樹S(T[0 .. n])只有n個葉子(其中tn=$)。(爲何呢,由於不可能存在子串S = T[j .. n]+c’,由於這要求子串中$以後還有字符,這是辦不到的),這也就是說整個建樹過程當中咱們一共只須要在尾部鏈表上處理n次就能夠了,這是一個好兆頭!
種種跡象代表咱們快到O(|T|)時間了,哈哈,原理就先說這麼多了。能不能實現最終的線性時間,就看下一節--線性時間內構建後綴樹!
1. http://www.cnblogs.com/snowberg/archive/2011/10/21/2468588.html