數據結構系列——後綴樹(附Java實現代碼)

後綴樹,說的通俗點就是將一個字符串所有的後綴按照前綴樹(Trie樹,可參考此篇文章)的形式組織成一棵樹。

什麼是後綴樹

舉例:“banana\0”,其中 “\0” 作爲文本結束符號,該字符串所有的後綴如下:

banana\0
 anana\0
  nana\0
   ana\0
    na\0
     a\0
      \0

將所有的後綴構建成一個前綴樹,如下:

圖1:粗陋的後綴樹:

圖1:粗陋的後綴樹

從圖中可以看出大量的重複子串,如存在三個“a\0”,浪費太多的空間,我們需要將其進行壓縮,得到如下的後綴樹:

圖2:真正的後綴樹:

圖2:真正的後綴樹

瞬間感覺看不懂了是吧,其實就是把每個節點放一個字符,改成放多個字符,比如圖1最左邊的一條分支,每個節點一個字符,太浪費,到圖2就成了一個節點,存儲了全部的”banana\0”字符,大大節省了空間。當然也增加了查找的複雜度。

後綴樹的應用

在看如何構建這樣一顆後綴樹之前,先了解下後綴樹的應用,如果沒有很好地應用場景,那麼我們就沒必要浪費時間去了解這麼一顆複雜的數據結構了。

  1. 查找字符串o是否在字符串S中。
    方案:用S構造後綴樹,按在trie中搜索字串的方法搜索o即可。
    原理:若o在S中,則o必然是S的某個後綴的前綴。
    例如S: leconte,查找o: con是否在S中,則o(con)必然是S(leconte)的後綴之一conte的前綴.有了這個前提,採用trie搜索的方法就不難理解了。
  2. 指定字符串T在字符串S中的重複次數。
    方案:用S+”$”構造後綴樹,搜索T節點下的葉節點數目即爲重複次數
    原理:如果T在S中重複了兩次,則S應有兩個後綴以T爲前綴,重複次數就自然統計出來了。
  3. 字符串S中的最長重複子串
    方案:原理同2,具體做法就是找到最深的非葉節點。
    這個深是指從root所經歷過的字符個數,最深非葉節點所經歷的字符串起來就是最長重複子串。
    爲什麼要非葉節點呢?因爲既然是要重複,當然葉節點個數要>=2。
  4. 兩個字符串S1,S2的最長公共部分
    方案:將S1#S2$作爲字符串壓入後綴樹,找到最深的非葉節點,且該節點的葉節點既有#也有$(無#)。

後面會用代碼來描述如何應用後綴樹進行字符串子串的查找,即應用1。下面先來看看後綴樹的構建算法。

後綴樹的構建

在 1995 年,Esko Ukkonen 發表了論文《On-line construction of suffix trees》,描述了在線性時間內構建後綴樹的方法。該文章當然是英文的了,不過國內已經有人翻譯了,翻譯的文章可以看這裏。不過說真的,這是論文,還是老外的論文,就算翻譯成中文,但是還是比較難以理解的,至少需要研究好幾遍才能大致明白,而且文中出現了比較多的感官動詞(不知道是不是這個名詞),如“一眼看出”,“顯然”等。所以我會結合這篇譯文,並加上自己的理解,希望以此幫助大家快速的理解。原文會通過正文對譯文進行原樣照搬,註釋採用斜體,請注意區分。

Ukkonen算法


本文試圖描述Ukkonen算法,首先顯示當字符串是簡單的(即不包含任何重複的字符)時候它做什麼,然後擴展到完整的算法。

首先來看一些前言:
我們正在建設的,基本上像一個搜索特里結構(單詞查找樹)。所以有一個根節點,從根節點引出新的節點,以及進一步從新節點引出其它節點,依次類推。

但是:與搜索單詞查找樹中不同,邊標籤不是單個字符。相反,每個邊的標籤是使用一對整數[從哪,到哪]。這些都是指向文本的指針。從這個意義上說,每個邊有任意長度的字符串標籤,但只需要O(1)的空間(兩個指針)。

基本原理

我想首先展示如何創建一個特別簡單的字符串的後綴樹,這個字符串沒有重複的字符,如:

abc

這個算法工作的步驟,從左到右。字符串的每個字符都有一個步驟。每一步都可能涉及到多個個體的操作,但是我們將會看到(見結尾最後的觀察)的總數量操作是O(n)。

所以我們從左開始,第一次插入單個字符“a“創建一個從根節點(在左邊)到一個葉節點的邊,和作爲[0,#)的標籤,這意味着邊代表了子串在位置0開始,結束在當前的結尾。我使用符號#來表示當前末尾,這是在位置1(a之後的右邊)。

因此,我們擁有一棵起始樹,這棵樹看起來如下:

這裏寫圖片描述

而其意義如下:

這裏寫圖片描述

現在我們前進到位置2(b的右邊)。 我們每個步驟的目標是插入至當前位置的所有後綴。
我們通過以下動作完成目標:

  • 擴展已存在邊a爲ab
  • 插入一條新邊b

在我們的圖示裏,它看起來如下:

這裏寫圖片描述

而其意義如下:

這裏寫圖片描述

我們看到了兩點:

  • ab邊的圖示與它在起始樹:[0,#]邊的圖示是相同的。它的意義卻已經自動更改了,因爲我們把當前的位置#從1更改到2。
  • 每條邊使用的空間複雜度爲O(1),因爲無論邊代表多少個字符 ,它都是由指向文本里的兩個指針組成。

接着我們再次增加位置,並且修改樹:給每個已經存在的邊增加c,插入一條表示新後綴c的邊。
在我們的圖示裏,它看起來如下:

這裏寫圖片描述

而其意義如下:

這裏寫圖片描述

我們看到

  • 這棵樹是經過上面的每個步驟後至當前位置的正確的後綴樹。
  • 步驟數目與文本中包含的字符一樣多。
  • 每個步驟的工作量是O(1),因爲所有已經存在的邊都是增加#來自動更改的,而且爲最後一個字符插入一條新邊的時間複雜度爲 O(1)。因此對一個長度爲n的字符串來說,只需要O(n)時間複雜度。

註釋1:到這裏論文中構建了一顆“abc”的後綴樹,按照步驟一步一步很容易理解,唯一的地方就是那個#,不過我們可以忽略這個#,在使用程序實現的時候我們會有一個很好的優化的方式讓我們很輕鬆的處理掉這個#。不過,從下面開始,事情似乎變得複雜了很多


第一次擴展:簡單的重複

當然,後綴樹表示的如此良好只是因爲我們的字符串沒有包含任何重複。現在我們看一個更真實的字符串:

abcabxabcd

這個字符串像前面例子裏一樣是abc開始的,接着重複ab ,緊跟着x,再接着重複abc,緊跟着d。
步驟1到3:經過了前三個步驟後,我們擁有了前面例子的那棵樹:

這裏寫圖片描述

步驟4:我們移動#到位置4。這隱含地更改所有已經存在的邊爲如下:

這裏寫圖片描述

而且我們需要在根節點插入當前步驟的最末的後綴a。
我們做這些之前,我們引入除#之外的 兩個或者更多變量,當然這些變量一直都存在,只是我們迄今爲止沒有使用它們:

  • 活動點(active point),它是一個三元組(活動節點,活動邊,活動長度)
  • 剩餘後綴數(reminder),它是一個整數,來說明我們需要插入多少個新的後綴。

這兩個圖示的確切含義不久就會清晰,不過現在我們只能說:

  • 在簡單的abc例子裏,活動點總是(root,’0x’,0),也就是說,活動節點是根節點,活動邊是由空字符’0x’指定的邊,活動長度是0。這麼做的結果是我們在每一步驟裏插入的一條新邊是作爲新創建的邊插入到根節點。不久我們就會明白爲什麼需要三元組表示這條信息。
  • 在每個步驟開始時剩餘後綴數總是設置爲1。它的意義是我們主動插入到每一步驟末尾的後綴數目是1(總是最後一個字符)。

註釋2:關於這裏的活動點和剩餘後綴數簡單解釋下。活動點中的活動節點:是用於查找一個後綴是否已經存在這棵樹裏,即查找的時候從活動節點的子節點開始查找,同時當需要插入邊的時候也是插入到該節點下;而活動邊則是每次需要進行分割的邊,即成爲活動邊就意味着需要被分割;而活動長度則是指明從活動邊的哪個位置開始分割。剩餘後綴數是我們需要插入的後綴的數量,說明程序員點就是緩存的數量,因爲每次如果要插入的後綴存在,則緩存起來。


現在將有變化了,當我們給根節點插入當前最後一個字符a的時候,我們特別注意到已經存在一條以a開始的邊:abca。在這種情況下我們做如下工作:

  • 我們不向根節點插入一條新邊[4,#]。相反,我們只是注意到後綴a已經在我們的樹裏。它終止在更長的邊的中間位置,不過這麼做我們並不疑惑,我們還是保留它們原來的樣子。
  • 我們設置活動點爲(root,’a’,1)。這意味着活動點現在是在根節點的以a開始的向外的邊的中間某個位置,具體地指這條邊的位置1之後。我們注意到這條邊只是由它的首個字符a來聲明的。這就足夠了,因爲以一個特定的字符開始的只有一條邊(通讀整個文檔之後可以確定這是真的)。
  • 我們還增加了剩餘後綴數, 那麼在下一步驟開始的時候,剩餘後綴數爲2。

注意:當發現我們需要插入的最終後綴已經存在在這棵樹裏的時候,這棵樹本身根本就沒有改變(我們只是修改了活動節點和剩餘後綴數)。那麼這棵樹就不再是能準確的表示至當前位置的後綴樹了,不過它包含了所有的後綴(因爲最終的後綴a隱含地包含了)。因此,除了修改變量外(所有這些變量都是定長的,因此空間複雜度是 O(1)),在這一步裏沒有做其他工作


註釋3:這裏原文用了我們特別注意到,可惜,程序可不會注意到,所以在實現的時候這裏會涉及到對活動節點的子節點的一個遍歷操作。這裏還更新了活動點,活動節點是root,而活動邊是’a’,對於程序來說活動邊則是一個邊的對象,只是這個邊包含的字符串時以’a’開頭,同時活動長度是從0增加到了1。剩餘後綴數是1,因爲我們緩存了一個後綴’a’。


步驟5:我們修改當前的位置#爲5。這將自動地如下更新這棵樹:

這裏寫圖片描述

而且由於剩餘後綴數爲2,我們需要插入目前位置的兩個最終後綴:ab和b。這主要是因爲:

  • 前一步驟的a後綴從來都沒有真正地插入。因此它保留下來,然而由於我們已經向前走了一步,它現在由a延長爲ab。
  • 還有,我們需要插入新的最終邊b。

實際上,這意味着我們要修改活動點(它現在指向的是abcab邊裏的a之後),而且插入當前的最後一個字符b, 不過:同時它也證明b也已經出現在同一條邊裏。

因此,我們再次不修改這棵樹,我們只是:

  • 修改活動點爲(root,’a’,2)(是與前面相同的節點和邊,只不過現在我們指向到b之後)
  • 增加剩餘後綴數爲3 ,因爲我們仍然不能插入前一個步驟的最終邊,同時我們也不能插入當前的最終邊

爲了清晰地說明:我們需要在當前的步驟裏插入ab和b,不過由於ab已經找到,所以我們修改了活動點,而且甚至不試圖插入b。爲什麼?因爲如果ab處於這棵樹裏,那麼它的每個後綴(包括b)也一定在這棵樹裏。也許僅僅是隱含性的,不過它一定在這棵樹裏,因爲這是我們迄今爲止建立這棵樹所採用的方法。


註釋4同時它也證明b也已經出現在同一條邊裏,看到這句話時思考了半天,真的無法證明,只是我們用肉眼看出來而已,所以程序需要做的是:比較活動邊在活動長度指定的字符與’b’是否相同,相同則證明了’b’也出現過在同一條邊。此時活動長度的作用就出來了,其實就是標識活動邊比較到哪裏了。


我們增加#而前進到步驟6。這棵樹自動修改爲:

這裏寫圖片描述

由於剩餘後綴數是3 ,我們不得不增加abx,bx和x。活動點告訴我們ab結束在哪兒,因此我們僅僅需要跳過這兒,然後插入x。x確實還不在這棵樹裏,因此我們分割abcabx邊,插入一個內部節點:

這裏寫圖片描述

這條邊表示的仍然是指向文本內部的指針,因此分割和插入內部節點的時間複雜度爲O(1)。


註釋5:這裏爲什麼突然不證明x也出現過在同一條邊呢?因爲被肉眼識破了,好吧,程序還是需要按照註釋4所說的方法進行一次比較,最終得出x不存在活動邊,所以如果需要插入abx,爲了充分利用空間,所以不會單獨建一個分支存放abx,而是將原來的abcabx分割成ab和cabx兩段,然後再爲ab增加一個分支存放x。而分割的邊就是活動邊,分割的長度就是活動長度。


這時我們處理了abx,並且把剩餘後綴數減爲2。現在我們需要插入下一個剩餘後綴bx。但是在我們做這些之前,我們需要修改活動節點。分割並插入一條邊遵循的規則稱作規則1,如下,而且它適用於活動節點是根節點的情況(針對下面後續的其他情況,我們將要了解規則3)。規則1如下:

向根節點插入遵循:

  • 活動節點保留爲根節點
  • 設置活動邊爲我們需要插入的新後綴的第一個字符,也就是b。
  • 活動長度減1

因此,新的活動節點三元組(root,’b’,1)表明要做的下一個插入在bcabx邊,第一個字符之後,即b之後。我們可以確定插入點的時間複雜度爲 O(1),並且檢查x是否已經出現在樹裏。如果它出現在這條邊裏,我們將結束當前的步驟,保持一切爲原樣。然而如果x沒有出現在這條邊裏,那麼我們分割這條邊而插入它:

這裏寫圖片描述


註釋6:上面進行了一次分割,所以引入了規則1,規則1的前提條件是向根節點插入,但是我們插入的是在a這個分支,其實我覺得應該是這麼理解:進行分割時如果活動節點是根節點,則依舊保留爲根節點;至於第二個 設置活動邊爲b,這個可不是這麼一句話就可以認定的,需要從活動節點進行一次查找,不過肯定是存在的,因爲存在ab,則必然存在b。如果剩餘的是bcx,則找到b之後還需要繼續找c,找x。最終找不到就分割,然後重複以上步驟即可。


再此說明,它的時間複雜度爲 O(1),而且我們按照規則1所示把剩餘後綴數修改爲1,活動節點修改爲(root,’x’,0)。

不過還有一件事情我們必須做。我們稱它爲規則2

如果我們分割一條邊並插入新的節點,而且如果它不是在當前步驟裏創建的第一個節點的話,我們通過特殊的指針,即後綴連接,把 以前插入的節點和新增的節點連接起來。後面我們將明白爲什麼這麼做是有用的。這兒我們要明白:後綴連接表示爲虛線邊


註釋7:後綴連接的目的是爲了方便後面進行查找,不過需要注意的是:是將前一個分割的節點通過後綴節點指向後一個分割的節點,而且這兩次分割必須是出現在一次插入中,即這裏是出現在插入x的情況下發生的兩次分割,所以可以增加後綴連接。


這裏寫圖片描述

我們仍然需要插入當前步驟的最終後綴x。因爲活動節點的活動長度部分已經減少到0,最終直接插入到根節點上。由於根節點上沒有以x開始的邊,所以我們插入了新邊:

這裏寫圖片描述

正如你所能看到的,在當前的步驟裏插入了所有剩餘的後綴。

我們設置#=7而前進到步驟7,這將像往常一樣自動添加下一個字符a到所有的葉子邊上。然後我們試圖插入新的最終字符到活動節點(根節點),然後發現它已經存在在這棵樹裏了。因此我們結束當前的步驟,不插入任何邊,並且修改活動點位(root,’a’,1)。

設置#=8進入步驟8,我們添加b,像以前所看到的,這僅僅意味着我們修改活動點位(root,’a’,2) ,而且不需要做其他事情就增加剩餘後綴數。因爲b已經出現在這棵樹裏。然而我們(在 O(1)時間複雜度裏)注意到活動節點現在是一條邊的結尾。我們通過重置活動節點位(node1,’\0x’,0)來體現這個。這兒,我們用node1來指ab邊結束的哪個內部節點。


註釋8:這裏出現了一個修改活動節點的規則:即如果活動邊上的所有字符全部都被匹配完了(級活動邊上的字符數==活動長度),則將活動邊晉升爲活動節點,同時重置活動長度爲0。所以下次查找時就得從該節點開始了,而不是根節點了。


接着設置#=9進入步驟9,我們需要插入’c’,這將有助於我們理解的最後一條技巧。

第二次擴展:使用後綴連接

像往常一樣,#的修改自動給每條是葉子的邊添加了c,而且我們轉到活動點看是否可以插入’c’。活動點顯示’c’已經存在在那條邊裏,因此我們設置活動點爲(node1,’c’,1),且增加剩餘後綴數,不做任何其他事情。

現在設置#=10進入步驟10,剩餘後綴數是4 ,因此我們首先需要在活動點插入d而實現插入abcd(這條邊從第三步驟開始就一直保留着)。

試圖在活動點插入d將引起時間複雜度爲O(1)的邊分割:

這裏寫圖片描述

分割起始的活動點在上圖中標記爲紅色。 最後一條規則即規則3如下:

分割從不是根節點的活動點開始的邊之後,我們應當緊跟着從活動點開始的後綴連接,如果存在一條這樣的連接,那麼重置活動節點使它指向這個節點。如果不存在這樣後綴連接,那麼我們設置活動節點爲根節點,活動邊和活動長度保持不變。

因此活動節點現在是(node2,’c’,1),這裏node2如下圖所示標記爲紅色:

這裏寫圖片描述


註釋9:這裏使用到了規則2,因爲分割c的時候,活動節點是ab,而非根節點,而ab正好存在後綴節點,所以分割完活動節點就跑到了ab的後綴節點b。其實添加了abcd之後,要插入的就是bcd了,如果不使用後綴節點,則需要從根節點開始找b,但是b就一個字符,所以活動節點還是會變成b,而後綴節點就是省了這一步,所以後綴節點只是一個優化手段而已。可以看到b分割之後就需要從根節點找c了,因爲b沒有後綴連接快速找到c。


由於abcd的插入已經完成,我們把剩餘後綴數減爲3,而且考慮當前步驟的下一個剩餘後綴bcd。規則3已經設置活動點爲右邊的節點和邊,因此插入bcd可以簡單地向活動點插入剩餘後綴的最後一個字符d來完成。
要做到這個將引起另一個邊分割,根據規則2 ,我們必須創建一條從以前已插入的節點開始的到新建節點的後綴連接:

這裏寫圖片描述

我們注意到:後綴連接使我們重置了活動點,因爲我們能在O(1)複雜度下插入下一個剩餘後綴。看看上面的圖就可確定標籤爲ab的真正節點連接到節點b(它的後綴),而節點abc則連接到bc節點。

當前步驟仍然沒有結束。現在剩餘後綴數是2,我們需要遵循規則3再次重置活動節點。由於當前的活動節點(上圖中紅色標記的)已經沒有後綴連接,我們重置活動節點位根節點。活動節點現在是(root,’c’,1)。

因此下一個插入發生在根節點的一條邊上,以c開始的這條邊的標籤爲:cabxabcd,位於第一個字符之後,即c之後。這將產生另一個分割:

這裏寫圖片描述

另外,由於這涉及到新的內部節點的創建,我們遵循規則2,設置一條新的從前面已創建的內部節點開始的後綴連接:

這裏寫圖片描述

(爲了製作這些小圖,我使用了Graphviz Dot軟件。新的後綴連接使得Dot軟件愛你重新佈局了已經存在的邊,因此仔細地檢查並確定上圖中插入的唯一的東西就是一條新的後綴連接。)

創建了這條連接,剩餘後綴樹可設置爲1 ,另外由於活動節點是根節點,我們根據規則1修改活動點位(root,’d’,0)。這意味着這一步的最後一個插入是向根節點插入單獨的d:

這裏寫圖片描述


註釋10:到這裏全部的步驟就結束了,後面的一些需要主要的地方都是這個算法需要注意的地方,所以需要先了解這個算法的運作原理才能看懂後面的注意。


這是最後一步,至此我們已經完成了後綴樹的建立。雖然工作已經完成,但還有許多最後要注意的地方:

  • 在每一步裏,我們向前移動#一個位置。這自動在時間複雜度O(1)內修改了所有的葉子結點。
  • 不過,後綴樹沒有處理 a) 前一步驟保留下來的任何後綴 b)和當前步驟的最後一個字符。
  • 剩餘後綴樹告訴我們我們需要做多少個後續的插入。這些插入把一對一對應爲在當前位置#結束的字符串的最後的後綴。我們認爲是一個接着一個,然後再對它們進行插入。重要的是:每條插入都在O(1)的時間複雜度內完成,因爲活動點告訴我們確切的位置,然後我們只需要在活動點增加一個單獨的字符。爲什 麼?因爲其他字符都隱含地包含了(否則活動點將是其他地方)。
  • 在做了每個這樣的插入之後,我們把剩餘後綴數減少,並且如果存在後綴的邊,就添加一條後綴連接。如果不存在,(根據規則3)我們把活動節點設置爲根節點。如果我們已經處在根節點,那麼我們根據規則1修改活動節點。在任何情況下,它只花費O(1)的時間複雜度。
  • 在任意插入期間,我們發現我們需要插入的字符已經存在,那麼我們不作任何事情而結束當前步驟,甚至在剩餘後綴樹大於0的情況下。理由是保留的任何插入都是我們試圖插入的邊的後綴。因此它們所有都隱藏在當前的樹裏。事實是剩餘後綴樹大於0確保我們後續對剩餘後綴的處理。
  • 如果在算法結束時剩餘後綴數大於0意味着什麼呢?將是這中情況:結束的文本是以前出現在某個地方的這個文本的子字符串。在這種 情況下,我們必須給這個字符串結尾添加一個額外以前沒有出現過的字符。在這樣的文檔裏,通常使用美元符號$作爲解決這個問題的符號。爲什麼會發生這種事情呢?—>如果後來我們使用完整的後綴樹搜尋後綴,那麼我們只有在後綴結束於葉子時才接受搜尋匹配。否則我們會得到許多假的匹配,因爲後綴樹立簡單地包含了不是豬字符串的真正後綴的許多這樣的字符串。在結束的時候強制剩餘後 綴數爲0是確保所有的後綴都結束在葉子節點的重要方法。然而,如果玩麼想用這棵樹來尋找通常的子字符串,而不僅僅是主字符串的後綴,那麼根據下面OP的評論的建議,最後一步確實不是必需的。
  • 那麼,整個算法的複雜性如何呢?如果文本是長度爲n的字符組成,那麼顯然需要n步(或者如果我們增加了沒有符號,那麼就是n+1 步)。在每個步驟裏,我們要麼(除了修改變量外)什麼都不做,要門我們插入剩餘的後綴,每一步都花費O(1)時間複雜度。由於剩餘後綴數表明了我們在以前的步驟裏不做任何事情的次數,而且現在我們每做一次插入就對剩餘後綴數遞減,我們做這樣的事情 總的次數準確地說是n(或者n+1)。因此,整體的複雜度是O(n)。
  • 然而,有一處小的地方我沒有正確地說明: 可能發生這樣的情況,我們添加了一條後綴連接,修改活動點,然後發現活動點的活動長度與新的活動節點不能一起正常工作。例如,看看下面這種情況:

    這裏寫圖片描述
    (短劃線指的是這棵樹的剩餘部分,虛線指的是後綴連接。)

現在,假設活動節點是(red,’d’,3),因此它指向def邊的f之後的位置。現在假設我們做了必須的修改,而且現在依據規則3續接了後綴連接並修改了活動節點。新的活動節點是(green,’d’,3)。然而從綠色節點出發的d邊是de,因此這條邊只有2個字符。爲了找到正確的活動點,很明顯我們需要添加一個到藍色節點的邊,然後重置活動節點爲(blue,’f’,1)。

在特別糟的情況下,活動長度可以是剩餘後綴數那麼大,它甚至可以與n一樣大。再在找正確的活動節點的時候,這種情況可能剛好發生,我們不僅僅需要跳過一個內部節點長度,不過也許很長,最壞的情況是高達n。由於在每一步裏 剩餘後綴的插入通常是O(n),續接了後綴之後的對活動節點的後續調整也是O(n)的複雜度 ,這是否意味着這個算法具有隱藏的O(n 2)的複雜度?

不是這樣的,理由是如果我們確實需要調整活動節點(例如,如上圖所示從綠色節點調整到藍色節點),那麼這就給我們引入了一個擁有自己的後綴連接的新節點,而且活動長度將縮減。當我們沿着後綴連接這個鏈向下走,我們就要插入剩餘的後綴,且只是縮減活動長度,使用這種方法我們可以調整的活動點的數目不可能超過任何給定時刻的活動長度。由於活動長度從來不會超過剩餘後綴數,而後綴剩餘數不僅僅在每個單一步驟裏是O(n),而且對整個處理過程進行的剩餘後綴遞增的總數也是O(n),因此調整活動節點的數目也是以O(n)爲界的。


註釋11:最後一點註釋,這裏的注意其實主要證明爲什麼算法時O(n),以及爲了某種特殊目的需要在字符串後面加一個$。



上面就是Ukkonen算法的全部內容,下面就是將其使用程序進行實現了,太長了,見下一篇吧~~~

參考文章

Ukkonen 的後綴樹算法的清晰解釋
後綴樹
從Trie樹(字典樹)談到後綴樹(10.28修訂)


歡迎訪問我的個人博客,尋找更多樂趣~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章