[算法]從Trie樹(字典樹)談到後綴樹

我是好文章的搬運工,原文來自博客園,博主July_,地址:http://www.cnblogs.com/v-July-v/archive/2011/10/22/2316412.htmlhtml

                                    從Trie樹(字典樹)談到後綴樹

做者:July、yansha。
出處: http://blog.csdn.net/v_JULY_v  。 

引言

    常關注本blog的讀者朋友想必看過此篇文章:從B樹、B+樹、B*樹談到R 樹此次,我們來說另外兩種樹:Tire樹與後綴樹。不過,在此以前,先來看兩個問題。
    第一個問題: 一個文本文件,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間複雜度分析。node

    以前在此文:海量數據處理面試題集錦與Bit-map詳解中給出的參考答案:用trie樹統計每一個詞出現的次數,時間複雜度是O(n*le)(le表示單詞的平均長度),而後是找出出現最頻繁的前10個詞。也能夠用堆來實現(具體的操做可參考第三章、尋找最小的k個數),時間複雜度是O(n*lg10)。因此總的時間複雜度,是O(n*le)與O(n*lg10)中較大的哪個。ios

    第二個問題:找出給定字符串裏的最長迴文。例子:輸入XMADAMYX。則輸出MADAM。這道題的流行解法是用後綴樹(Suffix Tree),但其用途遠不止如此,它能高效解決一大票複雜的字符串編程問題(固然,它有它的弱點,如算法實現複雜以及空間開銷大),歸納以下: 
c++

  • 查詢字符串S是否包含子串S1。主要思想是:若是S包含S1,那麼S1一定是S的某個後綴的前綴;又由於S的後綴樹包含了全部的後綴,因此只需對S的後綴樹使用和Trie相同的查找方法查找S1便可(使用後綴樹實現的複雜度同流行的KMP算法的複雜度至關)。 
  • 找出字符串S的最長重複子串S1。好比abcdabcefda裏abc同da都重複出現,而最長重複子串是abc。 
  • 找出字符串S1同S2的最長公共子串。注意最長公共子串(Longest CommonSubstring)和最長公共子序列(LongestCommon Subsequence, LCS)的區別:子串(Substring)是串的一個連續的部分,子序列(Subsequence)則是從不改變序列的順序,而從序列中去掉任意的元素而得到的新序列;更簡略地說,前者(子串)的字符的位置必須連續,後者(子序列LCS)則沒必要。好比字符串acdfg同akdfc的最長公共子串爲df,而他們的最長公共子序列是adf。LCS能夠使用動態規劃法解決。
  • Ziv-Lampel無損壓縮算法。 LZW算法的基本原理是利用編碼數據自己存在字符串重複特性來實現數據壓縮,因此一個很好的選擇是使用後綴樹的形式來組織存儲字符串及其對應壓縮碼值的字典。
  • 找出字符串S的最長迴文子串S1。例如:XMADAMYX的最長迴文子串是MADAM(此即爲上面所說的第二個問題:最長迴文問題,本文第二部分將詳細闡述此問題)。
  • 多模式串的模式匹配問題。(suffer_array+二分)。

   本文第一部分,我們就來了解這個Trie樹,而後天然而然過渡到第二部分、後綴樹,接着進入第三部分、詳細闡述後綴樹的構造方法-Ukkonen,最後第四部分、對自動機,KMP算法,Extend-KMP,後綴樹,後綴數組,trie樹,trie圖及其應用作個全文歸納性總結。權做此番闡述,以備不時之需,在須要的時候即可手到擒來。ok,有任何問題,歡迎不吝指正或賜教。謝謝。程序員

第一部分、Trie樹

什麼是Trie樹

    Trie樹,即字典樹,又稱單詞查找樹或鍵樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計和排序大量的字符串(但不只限於字符串),因此常常被搜索引擎系統用於文本詞頻統計。它的優勢是:最大限度地減小無謂的字符串比較,查詢效率比哈希表高。面試

    Trie的核心思想是空間換時間。利用字符串的公共前綴來下降查詢時間的開銷以達到提升效率的目的。

它有3個基本性質:算法

  1. 根節點不包含字符,除根節點外每個節點都只包含一個字符。
  2. 從根節點到某一節點,路徑上通過的字符鏈接起來,爲該節點對應的字符串。
  3. 每一個節點的全部子節點包含的字符都不相同。

樹的構建

舉個在網上流傳頗廣的例子,以下:
    題目:給你100000個長度不超過10的單詞。對於每個單詞,咱們要判斷他出沒出現過,若是出現了,求第一次出如今第幾個位置。
    分析:這題固然能夠用hash來解決,可是本文重點介紹的是trie樹,由於在某些方面它的用途更大。好比說對於某一個單詞,咱們要詢問它的前綴是否出現過。這樣hash就很差搞了,而用trie仍是很簡單。
    如今回到例子中,若是咱們用最傻的方法,對於每個單詞,咱們都要去查找它前面的單詞中是否有它。那麼這個算法的複雜度就是O(n^2)。顯然對於100000的範圍難以接受。如今咱們換個思路想。假設我要查詢的單詞是abcd,那麼在他前面的單詞中,以b,c,d,f之類開頭的我顯然沒必要考慮。而只要找以a開頭的中是否存在abcd就能夠了。一樣的,在以a開頭中的單詞中,咱們只要考慮以b做爲第二個字母的,一次次縮小範圍和提升針對性,這樣一個樹的模型就漸漸清晰了。
    比如假設有b,abc,abd,bcd,abcd,efg,hii 這6個單詞,咱們構建的樹就是以下圖這樣的:

 

 

  當時第一次看到這幅圖的時候,便立馬感到此樹之不凡構造了。單單從上幅圖即可窺知一二,比如大海搜人,立馬就能肯定東南西北中的到底哪一個方位,如此迅速縮小查找的範圍和提升查找的針對性,不失爲一創舉。
    ok,如上圖所示,對於每個節點,從根遍歷到他的過程就是一個單詞,若是這個節點被標記爲 色,就表示這個單詞存在,不然不存在。
    那麼,對於一個單詞,我只要順着他從根走到對應的節點,再看這個節點是否被標記爲紅色就能夠知道它是否出現過了。把這個節點標記爲紅色,就至關於插入了這個單詞。
    這樣一來咱們查詢和插入能夠一塊兒完成(重點體會這個查詢和插入是如何一塊兒完成的,稍後,下文具體解釋),所用時間僅僅爲單詞長度,在這一個樣例,即是10。
    咱們能夠看到,trie樹每一層的節點數是26^i級別的。因此爲了節省空間。咱們用動態鏈表,或者用數組來模擬動態。空間的花費,不會超過單詞數×單詞長度。

前綴查詢

    上文中提到」好比說對於某一個單詞,咱們要詢問它的前綴是否出現過。這樣hash就很差搞了,而用trie仍是很簡單「。下面,我們來看看這個前綴查詢問題:
    已知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)。(說白了,就是Trie樹的平均高度h爲len,因此Trie樹的查詢複雜度爲O(h)=O(len)。比如一棵二叉平衡樹的高度爲logN,則其查詢,插入的平均時間複雜度亦爲O(logN))。
    下面解釋下上述方法3中所說的爲何hash不能將創建與查詢同時執行,而Trie樹卻能夠:
  • 在hash中,例如如今要輸入兩個串911,911456,若是要同時查詢這兩個串,且查詢串的同時若hash中沒有則存入。那麼,這個查詢與創建的過程就是先查詢其中一個串911,沒有,而後存入九、9一、911;然後查詢第二個串911456,沒有而後存入九、9一、911911四、9114五、911456。由於程序沒有記憶功能,因此並不知道911在輸入數據中出現過,只是照常以例行事,存入九、9一、9十一、911四、911...。也就是說用hash必須先存入全部子串,而後for循環查詢。
  • 而trie樹中,存入911後,已經記錄911爲出現的字符串,在存入911456的過程當中就能發現而輸出答案;倒過來亦能夠,先存入911456,在存入911時,當指針指向最後一個1時,程序會發現這個1已經存在,說明911一定是某個字符串的前綴。
     讀者反饋@悠悠長風: 關於這點,我有不一樣的見解。hash也是能夠實現邊創建邊查詢的啊。當插入911時,須要一個額外的標誌位,表示它是一個完整的單詞。在處理911456時,也是按照前面的查詢9,91,911,當查詢911時,是能夠找到前面插入的911,且經過標誌位知道911爲一個完整單詞。那麼就能夠判斷出911爲911456的前綴啊。雖然trie樹更適合這個問題,可是我認爲hash也是能夠實現邊創建,邊查找。
    吾答曰:但若反過來呢?好比說是先查詢911456,然後查詢911呢?你的在hash中作一個完整單詞的標誌就行不通了。由於,你查詢911456時,並不知道後來911會是一個完整的單詞。
    至於,有關Trie樹的查找,插入等操做的實現代碼,網上遍地開花且千篇一概,諸君儘可參考,想必不用我再作多餘費神。

查詢

    Trie樹是簡單但實用的數據結構,一般用於實現字典查詢。咱們作即時響應用戶輸入的AJAX搜索框時,就是Trie開始。本質上,Trie是一顆存儲多個字符串的樹。相鄰節點間的邊表明一個字符,這樣樹的每條分支表明一則子串,而樹的葉節點則表明完整的字符串。和普通樹不一樣的地方是,相同的字符串前綴共享同一條分支。下面,再舉一個例子。給出一組單詞,inn, int, at, age, adv, ant, 咱們能夠獲得下面的Trie:

  能夠看出:
數據庫

  • 每條邊對應一個字母。
  • 每一個節點對應一項前綴。葉節點對應最長前綴,即單詞自己。
  • 單詞inn與單詞int有共同的前綴「in」, 所以他們共享左邊的一條分支,root->i->in。同理,ate, age, adv, 和ant共享前綴"a",因此他們共享從根節點到節點"a"的邊。

    查詢操縱很是簡單。好比要查找int,順着路徑i -> in -> int就找到了。

    搭建Trie的基本算法也很簡單,無非是逐一把每則單詞的每一個字母插入Trie。插入前先看前綴是否存在。若是存在,就共享,不然建立對應的節點和邊。好比要插入單詞add,就有下面幾步:
編程

  1. 考察前綴"a",發現邊a已經存在。因而順着邊a走到節點a。
  2. 考察剩下的字符串"dd"的前綴"d",發現從節點a出發,已經有邊d存在。因而順着邊d走到節點ad
  3. 考察最後一個字符"d",這下從節點ad出發沒有邊d了,因而建立節點ad的子節點add,並把邊ad->add標記爲d。

實現

Trie樹的單詞查詢實現    數組

    如下是trie樹的簡單實現。下圖所示的測試只是作了一個很是簡單的檢測而已,先插入j然後查找j,再刪除再查找,目的主要是看刪除函數是否有效。往後再好好寫下trie樹用於單詞頻率統計的實現(單詞統計hash表固然也能夠實現,只不過若是用trie樹統計單詞出現頻率,想象一下,當樹中已有某個單詞,再次遍歷到一樣的單詞,即可以迅速高效的查詢找到某個單詞,爲其出現計數+1,這得益於查找高效的所帶來的好處)。

//copyright@singmelody
//updated@2011 July
#include <stdio.h>
#include <malloc.h> 
#include <string.h> 
#define true 1 
#define false 0  

struct trieNode 
{ 
  trieNode():isword(false)     
  {         
    memset(next, 0, sizeof(next));   
  }    
  trieNode *next[26];    
  bool isword; 
}Root;  

void insert(char *tar)
{    
  trieNode *p =&Root;    
  int id;     
  while(*tar)    
  {       
    id = *tar-'a';             
    if(p->next[id] == NULL)     
    {          
      p->next[id] =(trieNode *)malloc(sizeof(trieNode));     
    }        
    p = p->next[id];          
    tar++;    
  }    
  p->isword = true; 
}   

//找到返回 true 不然返回false 
int search(char *tar) 
{    
  trieNode *p = &Root;    
  int id;     
  while(*tar)     
  {        
    id = *tar - 'a';       
    if (p->next[id] == NULL)      
    {            
      return false;      
    }        
    p = p->next[id];     
    tar++;    
}       

  //判斷結點是否標記    
  if (p->isword == true)    
    return true;   
  else       
    return false; 
}   

void remove(char *tar)
{    
  trieNode *p =&Root;     
  int id;    
  while(*tar)   
  {        
    id = *tar-'a';       
    p = p->next[id];      
    tar++;   
  }    
  p->isword = false;
}   

void searchprocess()
{    
  char searchstr[20];     
  printf("Please search:\n");     
  scanf("%s",searchstr);    
  printf("Now searching %s:\n",searchstr);      
  if (search(searchstr)==true)  
  {       
    printf("Success\n");    
  }     
  else  
  {        
    printf("Fail\n");   
  } 
}  

int main()
{    
          //.....
  return 0; 
}

 

Trie樹單詞頻率統計實現

    如下是用Trie樹統計單詞頻率的實現,程序尚不完善,有不少地方還需改進。

// trie tree.cpp : 定義控制檯應用程序的入口點。
//
#include "stdafx.h"
//功能:統計一段英文的單詞頻率(文章以空格分隔,沒有標點) 
//思路:trie節點保存單詞頻率,而後經過DFS按字典序輸出詞頻 
//時空複雜度: O(n*len)(len爲單詞平均長度)
//copyright@yansha 2011.10.25
//updated@July 2011.10.26
//程序尚不完善,有不少地方還需改進。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>

#define num_of_letters 26
#define max_word_length 20

// 定義trie樹節點
struct Trie   
{   
  int count;   
  Trie *next[num_of_letters]; 
};   

// 定義根節點 
Trie *root = NULL;

/**
* 創建trie樹,同時保存單詞頻率
*/
void create_trie(char *word)
{
  int len = strlen(word);
  Trie *cur = root, *node;
  int pos = 0;

  // 深度爲單詞長度
  for(int i = 0; i < len; ++i)
  {
    // 將字母範圍映射到0-25之間
    pos = word[i] - 'a';

    // 若是當前字母沒有對應的trie樹節點則創建,不然處理下一個字母
    if(cur->next[pos] == NULL)   //一、這裏應該有個查找過程
    {
      node = (Trie *)malloc(sizeof(Trie));
      node->count = 0;

      // 初始化next節點
      for(int j = 0; j < num_of_letters; ++j)
      node->next[j] = NULL;

      // 開始處理下一個字母
      cur->next[pos] = node;
      }
  cur = cur->next[pos];
  }
  // 單詞頻率加1
  cur->count++;
}

/**
* 大寫字母轉化成小寫字母
*/
void upper_to_lower(char *word, int len)
{
  for (int i = 0; i < len; ++i)
  {
    if(word[i] >= 'A' && word[i] <= 'Z')
    word[i] += 32;
  }
}

/**
* 處理輸入
*/
void process_input()
{
  char word[max_word_length];

  // 打開統計文件(注意保持文件名一致)
  FILE *fp_passage = fopen("passage.txt", "r");  
  assert(fp_passage);

  // 循環處理單詞
  while (fscanf(fp_passage, "%s", word) != EOF)  
  {  
    int len = strlen(word);  
    if (len > 0)  
      upper_to_lower(word, len);  
    create_trie(word);  
  }  
  fclose(fp_passage);  
}

/**
* 深度優先遍歷
*/ 
void trie_dfs(Trie *p, char *queue)
{
  for(int i = 0; i < num_of_letters; ++i)
  {
    if(p->next[i] != NULL)
  {
    // 定義隊列頭結點
    char *head = queue;

    // 在末尾增長一個字母
    while (*queue != '\0')
      queue++;
    *queue = (char)(i + 'a');
    queue = head;

    // 在控制檯打印單詞及其頻率
    if (p->next[i]->count > 0)
      printf("%s\t%d\n", queue, p->next[i]->count);

    trie_dfs(p->next[i], queue);

    // 在末尾去掉一個字母
    head = queue;
    while (*(queue+1) != '\0')
      queue++;
    *queue = '\0';
    queue = head;
    }
  }
}

int main()
{
  // 初始化trie樹根節點
  root = (Trie *)malloc(sizeof(Trie));
  for(int j = 0; j < num_of_letters; ++j)
  root->next[j] = NULL;

  // 處理輸入
  process_input();

  // 分配一個保存單詞中間結果的隊列
  char *queue = (char*) calloc(max_word_length, sizeof(char));

  // 經過DFS打印結果
  trie_dfs(root, queue);
  system("pause");
  return 0;
}

 

Trie樹的應用

    除了本文引言處所述的問題能應用Trie樹解決以外,Trie樹還能解決下述問題(節選自此文: 海量數據處理面試題集錦與Bit-map詳解):
    • 三、有一個1G大小的一個文件,裏面每一行是一個詞,詞的大小不超過16字節,內存限制大小是1M。返回頻數最高的100個詞。
    • 九、1000萬字符串,其中有些是重複的,須要把重複的所有去掉,保留沒有重複的字符串。請怎麼設計和實現?
    • 十、 一個文本文件,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間複雜度分析。
    • 1三、尋找熱門查詢:
    • 搜索引擎會經過日誌文件把用戶每次檢索使用的全部檢索串都記錄下來,每一個查詢串的長度爲1-255字節。假設目前有一千萬個記錄,這些查詢串的重複讀比較高,雖然總數是1千萬,可是若是去除重複和,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的用戶越多,也就越熱門。請你統計最熱門的10個查詢串,要求使用的內存不能超過1G。

      (1) 請描述你解決這個問題的思路;

    (2) 請給出主要的處理流程,算法,以及算法的複雜度。
    有了Trie,後綴樹就容易理解了。本文接下來的第二部分,介紹後綴樹。

第二部分、後綴樹

後綴樹的定義    

    後綴樹(Suffix tree)是一種數據結構,能快速解決不少關於字符串的問題。後綴樹的概念最先由Weiner 於1973年提出,既而由McCreight 在1976年和Ukkonen在1992年和1995年加以改進完善。

    後綴,顧名思義,甚至通俗點來講,就是所謂後綴就是後面尾巴的意思。好比說給定一長度爲n的字符串S=S1S2..Si..Sn,和整數i,1 <= i <= n,子串SiSi+1...Sn便都是字符串S的後綴。

    以字符串S=XMADAMYX爲例,它的長度爲8,因此S[1..8], S[2..8], ... , S[8..8]都算S的後綴,咱們通常還把空字串也算成後綴。這樣,咱們一共有以下後綴。對於後綴S[i..n],咱們說這項後綴起始於i。

S[1..8], XMADAMYX, 也就是字符串自己,起始位置爲1
  S[2..8], MADAMYX,起始位置爲2
     S[3..8], ADAMYX,起始位置爲3
       S[4..8], DAMYX,起始位置爲4
          S[5..8], AMYX,起始位置爲5
            S[6..8], MYX,起始位置爲6
               S[7..8], YX,起始位置爲7
                 S[8..8], X,起始位置爲8
                                 空字串,記爲$。

    然後綴樹,就是包含一則字符串全部後綴的壓縮Trie。把上面的後綴加入Trie後,咱們獲得下面的結構:

    仔細觀察上圖,咱們能夠看到很多值得壓縮的地方。好比藍框標註的分支都是獨苗,沒有必要用單獨的節點同邊表示。若是咱們容許任意一條邊裏包含多個字 母,就能夠把這種沒有分叉的路徑壓縮到一條邊。另外每條邊已經包含了足夠的後綴信息,咱們就不用再給節點標註字符串信息了。咱們只須要在葉節點上標註上每 項後綴的起始位置。因而咱們獲得下圖:

    這樣的結構丟失了某些後綴。好比後綴X在上圖中消失了,由於它正好是字符串XMADAMYX的前綴。爲了不這種狀況,咱們也規定每項後綴不能是其它後綴的前綴。要解決這個問題其實挺簡單,在待處理的子串後加一個空字串就好了。例如咱們處理XMADAMYX前,先把XMADAMYX變爲 XMADAMYX$,因而就獲得suffix tree--後綴樹了,以下圖所示:

後綴樹與迴文問題的關聯

    那後綴樹同最長迴文有什麼關係呢?咱們得先知道兩個簡單概念:

 

  1. 最低共有祖先,LCA(Lowest Common Ancestor),也就是任意兩節點(多個也行)最長的共有前綴。好比下圖中,節點7同節點10的共同祖先是節點1與借點,但最低共同祖先是5。 查找LCA的算法是O(1)的複雜度,這年頭少見。代價是須要對後綴樹作複雜度爲O(n)的預處理。 
  2. 廣義後綴樹(Generalized Suffix Tree)。傳統的後綴樹處理一坨單詞的全部後綴。廣義後綴樹存儲任意多個單詞的全部後綴。例以下圖是單詞XMADAMYX與XYMADAMX的廣義後綴 樹。注意咱們須要區分不一樣單詞的後綴,因此葉節點用不一樣的特殊符號與後綴位置配對。 

 

 

最長迴文問題的解決

    有了上面的概念,本文引言中提出的查找最長迴文問題就相對簡單了。我們來回顧下引言中提出的迴文問題的具體描述:找出給定字符串裏的最長迴文。例如輸入XMADAMYX,則輸出MADAM。

 

    思惟的突破點在於考察迴文的半徑,而不是迴文自己。所謂半徑,就是迴文對摺後的字串。好比迴文MADAM 的半徑爲MAD,半徑長度爲3,半徑的中心是字母D。顯然,最長迴文必有最長半徑,且兩條半徑相等。仍是以MADAM爲例,以D爲中心往左,咱們獲得半徑 DAM;以D爲中心向右,咱們獲得半徑DAM。兩者確定相等。由於MADAM已是單詞XMADAMYX裏的最長迴文,咱們能夠確定從D往左數的字串 DAMX與從D往右數的子串DAMYX共享最長前綴DAM。而這,正是解決迴文問題的關鍵。如今咱們有後綴樹,怎麼把從D向左數的字串DAMX變成後綴 

    到這個地步,答案應該明顯:把單詞XMADAMYX翻轉(XMADAMYX=>XYMADAMX,DAMX就變成後綴了就好了。因而咱們把尋找回文的問題轉換成了尋找兩坨後綴的LCA的問題。固然,咱們還須要知道 到底查詢那些後綴間的LCA。很簡單,給定字符串S,若是最長迴文的中心在i,那從位置i向右數的後綴恰好是S(i),而向左數的字符串恰好是翻轉S後獲得的字符串S‘的後綴S'(n-i+1)。這裏的n是字符串S的長度。

    可能上面的闡述還不夠直觀,我再細細說明下:

    一、首先,還記得本第二部分開頭關於後綴樹的定義麼: 「先說說後綴的定義,顧名思義,甚至通俗點來講,就是所謂後綴就是後面尾巴的意思。好比說給定一長度爲n的字符串S=S1S2..Si..Sn,和整數i,1 <= i <= n,子串SiSi+1...Sn便都是字符串S的後綴。」

 

    以字符串S=XMADAMYX爲例,它的長度爲8,因此S[1..8], S[2..8], ... , S[8..8]都算S的後綴,咱們通常還把空字串也算成後綴。這樣,咱們一共有以下後綴。對於後綴S[i..n],咱們說這項後綴起始於i。

S[1..8], XMADAMYX, 也就是字符串自己,起始位置爲1
  S[2..8], MADAMYX,起始位置爲2
     S[3..8], ADAMYX,起始位置爲3
       S[4..8], DAMYX,起始位置爲4
          S[5..8], AMYX,起始位置爲5
            S[6..8], MYX,起始位置爲6
               S[7..8], YX,起始位置爲7
                 S[8..8], X,起始位置爲8
                                  空字串,記爲$。

    二、對單詞XMADAMYX而言,迴文中心爲D,那麼D向右的後綴DAMYX假設是S(i)(當N=8,i從1開始計數,i=4時,即是S(4..8));而對於翻轉後的單詞XYMADAMX而言,迴文中心D向右對應的後綴爲DAMX,也就是S'(N-i+1)((N=8,i=4,即是S‘(5..8)) 。此刻已經能夠得出,它們共享最長前綴,即LCA(DAMYX,DAMX)=DAM。有了這套直觀解釋,算法天然呼之欲出:

  1. 預處理後綴樹,使得查詢LCA的複雜度爲O(1)。這步的開銷是O(N),N是單詞S的長度 ;
  2. 對單詞的每一位置i(也就是從0到N-1),獲取LCA(S(i), S‘(N-i+1)) 以及LCA(S(i+1), S’(n-i+1))。查找兩次的緣由是咱們須要考慮奇數迴文和偶數迴文的狀況。這步要考察每坨i,因此複雜度是O(N) ;
  3. 找到最大的LCA,咱們也就獲得了迴文的中心i以及迴文的半徑長度,天然也就獲得了最長迴文。總的複雜度O(n)。 

    用上圖作例子,i爲4時,LCA(4$, 5#)爲DAM,正好是最長半徑。固然,這只是直觀的敘述。
    上面大體描述了後綴樹的基本思路。要想寫出實用代碼,至少還得知道下面的知識:

  • 建立後綴樹的O(n)算法。此算法有不少種,不管Peter Weiner的73年年度最佳算法,仍是Edward McCreight1976的改進算法,仍是1995年E. Ukkonen大幅簡化的算法(本文第4部分將重點闡述這種方法),仍是Juha Kärkkäinen 和 Peter Sanders2003年進一步簡化的線性算法,都是O(n)的時間複雜度。至於實際中具體選擇哪種算法,可依實際狀況而定。 
  • 實現後綴樹用的數據結構。好比經常使用的子結點加兄弟節點列表,Directed 優化後綴樹空間的辦法。好比不存儲子串,而存儲讀取子串必需的位置。以及Directed Acyclic Word Graph,常縮寫爲黑哥哥們掛在嘴邊的DAWG。 

後綴樹的應用

    後綴樹的用途,總結起來大概有以下幾種 

 

  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$做爲字符串壓入後綴樹,找到最深的非葉節點,且該節點的葉節點既有#也有$(無#)。 

    後綴樹的代碼實現,下期再續。第二部分、後綴樹完。

第三部分、後綴樹的構造方法-Ukkonen

    接下來,我們來了解後綴樹的構造方法-Ukkomen。爲了兼顧上文內容,以及加深印象,本部分打算從Trie樹從頭到位從新開始闡述一切。

    Ukkonen的構造法O(n), 它比Sartaj Sahni的構造法O(nr), r爲字母表大小 在時間上更有優點. 但咱們不能說Sartaj Sahni的算法慢, 由於r每每會很小, 所以實際效率也接近線性, 兩種構造法在思想上均有可取之處.

 

問題的起源

 

字符串匹配問題是程序員常常要面對的問題. 字符串匹配算法的改進能夠使許多工程受益良多, 好比數據壓縮和DNA排列。你能夠把本身想象成一名工做於DNA排列工程的程序員. 那些基因研究者們每天忙着分切病毒的基因材料, 製造出一段一段的核苷酸序列. 他們把這些序列發到你的服務器裏, 期望你在基因數據庫中定位. 要知道, 你的數據庫裏有數百種病毒的數據, 而一個特定的病毒能夠有成千上萬的鹼基. 你的程序必須像C/S工程那樣實時向博士們反饋信息, 這須要一個很好的方案。

 很明顯, 在這個問題上採起暴力算法是極其低效的. 這種方法須要你在基因數據庫裏對比每個核苷酸, 測試一個較長的基因段基本會把你的C/S系統變成一臺古老的批處理機。

直覺上的解決方法

因爲基因數據庫通常是不變的, 經過預處理來把搜索簡化或許是個好主意. 一種預處理的方法是創建一棵Trie. 咱們經過Trie引伸出一種東西叫做後綴Trie. (後綴Trie離後綴樹僅一步之遙.) 首先, Trie是一種n叉樹, n爲字母表大小, 每一個節點表示從根節點到此節點所通過的全部字符組成的字符串. 然後綴Trie的 「後綴」 說明這棵Trie包含了所給字段的全部後綴 (也許正是一個病毒基因).

 

 

圖1 BANANAS的後綴Trie

上展現了文本BANANAS的後綴Trie. 關於這棵Trie有兩個地方須要注意. 第一, 從根節點開始, BANANAS的每個後綴都插入到Trie中, 包括BANANAS, ANANAS, NANAS, ANAS, NAS, AS, S. 第二, 鑑於這種結構, 你能夠經過從根節點往下匹配的方式搜索到單詞的任何一個子串.

這裏所說的第二點正是咱們認爲後綴Trie優秀的緣由. 若是你輸入一個長度爲N的文本並想在其中搜索一個長度爲M的串, 傳統的暴力匹配須要進行N*M次字符對比, 而一些改進過的匹配技術, 好比像Boyer-Moore算法, 能夠在O(N+M)的時間開銷內解決問題, 平均效率更是使人滿意. 然而, 後綴Trie亮出了O(M)的牌子, 完全鄙視了其餘算法的成績, 後綴Trie對比的次數僅僅至關於被搜索串的長度!

這確實是可圈可點的威力, 這意味着你能經過僅僅7次對比便在莎士比亞全部做品中找出BANANAS. 但有一點咱們可不能忘了, 構造後綴Trie也是須要時間的.

後綴Trie之因此沒有家喻戶曉正是由於構造它須要O(n2)的時間和空間. 平方級的開銷使它在最須要它的領域 --- 長串搜索 中被拒之門外.

橫空出世

直到1976年, Edward McCreigh發表了一篇論文, 我們的後綴樹問世了. 後綴Trie的困境被完全打破.

後綴樹跟後綴Trie有着同樣的佈局, 但它把只有一個兒子的節點給剔除了. 這個過程被稱爲路徑壓縮, 這意味着樹上的某些邊將表示一個序列而不是單獨的字符.

圖2   BANANAS的後綴樹

圖2是由圖1的後綴Trie轉化而來的後綴樹. 你會發現這樹基本仍是那個形狀, 只是節點變少了. 在剔除了只有一個兒子的節點以後, 總節點數由23降爲11. 通過證實, 在最壞狀況下, 後綴樹的節點數也不會超過2N (N爲文本的長度). 這使構造後綴樹的線性時空開銷成爲可能.

然而, McCreight最初的構造法是有些缺陷的, 原則上它要按逆序構造, 也就是說字符要從末端開始插入. 如此一來, 便不能做爲在線算法, 它變得更加難以應用於實際問題, 如數據壓縮.

20年後, 來自赫爾辛基理工大學的Esko Ukkonen把原算法做了一些改動, 把它變成了從左往右. 本文接下來的全部描述和代碼都是基於Esko Ukkonen的成果.

對於所給的文本T, Esko Ukkonen的算法是由一棵空樹開始, 逐步構造T的每一個前綴的後綴樹. 好比咱們構造BANANAS的後綴樹, 先由B開始, 接着是BA, 而後BAN, … . 不斷更新直到構造出BANANAS的後綴樹.

圖3  逐步構造後綴樹


初窺門徑

加入一個新的前綴須要訪問樹中已有的後綴. 咱們從最長的一個後綴開始(圖3中的BAN), 一直訪問到最短的後綴(空後綴). 每一個後綴會在如下三種節點的其中一種結束.

 

  • 一個葉節點. 這個是常識了, 圖4中標號爲1, 2, 4, 5的就是葉節點.
  • 一個顯式節點. 圖4中標號爲0, 3的是顯式節點, 它表示該節點以後至少有兩條邊.
  • 一個隱式節點. 圖4中, 前綴BO, BOO, 或者非前綴OO, 它們都在某條表示序列的邊上結束, 這些位置就叫做隱式節點. 它表示後綴Trie中存在的因爲路徑壓縮而剔除的節點. 在後綴樹的構造過程當中, 有時要把一些隱式節點轉化爲顯式節點。

 

 

        圖4  加入BOOK以後的BOOKKEEPER

(也就是BOOK的後綴樹)

如圖4, 在加入BOOK以後, 樹中有5個後綴(包括空後綴). 那麼要構造下一個前綴BOOKK的後綴樹的話, 只須要訪問樹中已存在的每個後綴, 而後在它們的末尾加上K.

前4個後綴BOOK, OOK, OK和K都在葉節點上結束. 因爲咱們要路徑壓縮, 只須要在通往葉節點的邊上直接加一個字符, 而不須要建立一個新節點.

在全部葉節點更新以後, 咱們還須要在空後綴後面加上K. 這時候咱們發現已經存在一條從0節點出發的邊的首字符爲K, 不必多此一舉了. 換句話說, 新加入的後綴K能夠在0節點和2節點之間的隱式節點中找到. 最終形態見圖5.

       

       圖5 加入BOOKK以後的BOOKKEEPER

相比圖4, 樹的結構沒有發生變化

若是你是一位敏感的讀者, 可能要發問了, 若是加入K咱們什麼都不作的話, 在查找的時候如何知道它究竟是一個後綴呢仍是某個後綴的一截? 若是你同時又是一位熟悉字符串算法的朋友, 內心可能立刻就有答案了 --- 咱們只須要在文本後面加個字母表之外的字符, 好比$或者#. 那咱們查找到K$或K#的話就說明這是一個後綴了.

稍微麻煩一點的事情

從圖4到圖5這個更新過程是相對簡單的, 其中咱們執行了兩種更新: 一種是將某條邊延長, 另外一種是啥都不作. 但接下來往圖5繼續加入BOOKKE, 咱們則會遇到另外兩種更新:

  1. 建立一個新節點來割開某一隱式節點所處的邊, 並在其後加一條新邊.
  2. 在顯式節點後加一條新邊.

 

圖6先分割, 再添加

當咱們往圖5的樹中加入BOOKKE的時候, 咱們是從已存在的最長後綴BOOKK開始, 一直操做到最短的後綴空後綴. 更新最長的後綴必然是更新葉節點, 以前提到了, 很是簡單. 除此以外, 圖5中結束在葉節點上的後綴還有OOKK, OKK, KK. 圖6的第一棵樹展現了這一類節點的更新.

圖5中首個不是結束在葉節點上的後綴是K. 這裏咱們先引入一個定義:

在每次更新後綴樹的過程當中, 第一個非葉節點稱爲激活節點. 它有如下性質:

 

  1. 全部比激活節點長的後綴都在葉節點上結束.
  2. 全部在激活節點以後加入的後綴都不在葉節點上結束.

 

後綴K在邊KKE上的隱式節點結束. 在後綴樹中咱們要判斷一個節點是否是非葉節點須要看它是否有跟待加入字符相同的兒子, 即本例中的E.

一眼能夠看出, KKE中的第一個K只有一個兒子: K. 因此它是非葉節點(這裏同時也是激活節點), 咱們要給他加一個兒子來表示E. 這個過程有兩個步驟:

 

  1. 在第一個K和第二個K之間把邊分割開, 因而第一個K(隱式節點)成了一個顯式節點, 如圖6第二棵樹.
  2. 在剛剛變身而來的顯式節點後加一個新節點表示E, 如圖6第三棵樹. 由此咱們又多了一個葉節點。

 

後綴K更新以後, 別忘了還有空後綴. 空後綴在根節點(節點0)結束, 顯然此時根節點是一個顯式節點. 咱們看一下它後面有沒有以E開頭的邊---沒有, 那麼加入一個新的葉節點(若是存在以E開頭的邊, 則不用任何操做). 最終如圖7.

 

圖7

概括, 反思, 優化

藉助後綴樹的特性, 咱們能夠作出一個至關有效的算法. 首先一個重要的特性是: 一朝爲葉, 終生爲葉. 一個葉節點自誕生之後毫不會有子孫. 更重要的是, 每當咱們往樹上加入一個新的前綴, 每一條通往葉節點的邊都會延長一個字符(新前綴的最後一個字符). 這使得處理通往葉節點的邊變得異常簡單, 咱們徹底能夠在建立葉節點的時候就把當前字符到文本末的全部字符一股腦塞進去. 是的, 咱們不須要知道後面的字符是啥, 但咱們知道它們最終都要被加進去. 所以, 一個葉節點誕生的時候, 也正是它能夠被咱們遺忘的時候. 你可能會擔憂通往葉節點的邊被分割了怎麼辦, 那也沒關係, 分割以後只是起點變了, 尾部該怎麼着仍是怎麼着.

如此一來, 咱們只須要關心顯式節點和隱式節點上的更新.

還要提到一個節約時間的方法. 當咱們遍歷全部後綴時, 若是某個後綴的某個兒子跟待加字符(新前綴最後一個字符)相同, 那麼咱們當前前綴的全部更新就能夠中止了. 若是你理解了後綴樹的本質, 你會知道一旦待加字符跟某個後綴的某個兒子相同, 那麼更短的後綴必然也有這個兒子. 咱們不妨把首個這樣的節點定義爲結束節點. 比結束節點長的後綴必然是葉節點, 這一點很好解釋, 要麼原本就是葉節點, 要麼就是新建立的節點(新建立的必然是葉節點). 這意味着, 每個前綴更新完以後, 當前的結束節點將成爲下一輪更新的激活節點.

好了, 如今咱們能夠把後綴樹的更新限制在激活節點和結束節點之間, 效率有了很大的改善. 整理成僞代碼以下:

Update( 新前綴 )
{
  當先後綴 = 激活節點
  待加字符 = 新前綴最後一個字符
       done = false;
  while ( !done ) {
  if ( 當先後綴在顯式節點結束 ) 
  {
    if ( 當前節點後沒有以待加字符開始的邊 )
      在當前節點後建立一個新的葉節點
    else
      done = true;
  } else {
    if ( 當前隱式節點的下一個字符不是待加字符 ) 
    {
      從隱式節點後分割此邊
       在分割處建立一個新的葉節點
    } else
      done = true;
    if ( 當先後綴是空後綴 )
      done = true;
    else
      當先後綴 = 下一個更短的後綴
     }
  激活節點 = 當先後綴
}

 

後綴指針

上面的僞代碼看上去很完美, 但它掩蓋了一個問題. 注意到第21行, 「下一個更短的後綴」, 若是呆板地沿着樹枝去搜索咱們想要的後綴, 那這種算法就不是線性的了. 要解決此問題, 咱們得附加一種指針: 後綴指針. 後綴指針存在於每一個結束在非葉節點的後綴上, 它指向「下一個更短的後綴」. 即, 若是一個後綴表示文本的第0到第N個字符, 那麼它的後綴指針指向的節點表示文本的第1到第N個字符.

圖8是文本ABABABC的後綴樹. 第一個後綴指針在表示ABAB的節點上. ABAB的後綴指針指向表示BAB的節點. 一樣地, BAB也有它的後綴指針, 指向AB. 如此這般.

 

圖8 加上後綴指針(虛線)的ABABABC的後綴樹

介紹一下如何建立後綴指針. 後綴指針的建立是跟後綴樹的更新同步的. 隨着咱們從激活節點移動到結束節點, 我把每一個新的葉節點的父親的路徑保存下來. 每當建立一條新邊, 我同時也在上一個葉節點的父親那兒建立一個後綴指針來指向當前新邊開始的節點. (顯然, 咱們不能在第一條新邊上作這樣的操做, 但除此以外均可以這麼作.)

有了後綴指針, 就能夠方便地一個後綴跳到另外一個後綴. 這個關鍵性的附加品使得算法的時間上限成功降爲O(N)。

第四部分、全文總結
自動機,KMP算法,Extend-KMP,後綴樹,後綴數組,trie樹,trie圖及其應用

    涉及到字符串的問題,無外乎這樣一些算法和數據結構:自動機,KMP算法,Extend-KMP,後綴樹,後綴數組,trie樹,trie圖及其應用。固然這些都是比較高級的數據結構和算法,而這裏面最經常使用和最熟悉的大概是kmp,即便如此仍是有至關一部分人也不理解kmp,更別說其餘的了。固然通常的字符串問題中,咱們只要用簡單的暴力算法就能夠解決了,而後若是暴力效率過低,就用個hash。固然hash也是一個面試中常常被用到的方法。這樣看來,這樣的一些算法和數據結構實際上不多會被問到,不過若是使用它們通常能夠獲得很好的線性複雜度的算法。

    老實說,字符串問題的確挺複雜的,出來一個若是用暴力,hash搞不定,就很難再想其餘的方法,固然有些能夠用動態規劃。下圖主要說明下這些算法數據結構之間的關係。圖中黃色部分主要寫明瞭這些算法和數據結構的一些關鍵點。

    圖中能夠看到這樣一些關係:extend-kmp 是kmp的擴展;ac自動機是kmp的多串形式;它是一個有限自動機;而trie圖其實是一個肯定性有限自動機;ac自動機,trie圖,後綴樹實際上都是一種trie;後綴數組和後綴樹都是與字符串的後綴集合有關的數據結構;trie圖中的後綴指針和後綴樹中的後綴連接這兩個概念及其一致。
    KMP算法請參考本博客內的這兩篇文章:6、教你從頭至尾完全理解KMP算法、updated六(續)、從KMP算法一步一步談到BM算法

    後綴樹的構造能夠用Ukkonen算法在線性時間內完成[,可是不只構造算法實現至關複雜,並且後綴樹存在着致命弱點:空間開銷大且對大字母表時間效率不理想。至於後綴數組下次闡述,這裏簡單介紹下extend-kmp。而在介紹extend-kmp以前,我們先要回顧下KMP算法。

 kmp

    首先這個匹配算法,主要思想就是要充分利用上一次的匹配結果,找到匹配失敗時,模式串能夠向前移動的最大距離。這個最大距離,必需要保證不會錯過可能的匹配位置,所以這個最大距離實際上就是模式串當前匹配位置的next數組值。也就是max{Aj 是 Pi 的後綴  j < i},pi表示字符串A[1...i],Aj表示A[1...j]。模式串的next數組計算則是一個自匹配的過程。也是利用已有值next[1...i-1]計算next[i]的過程。咱們能夠看到,若是A[i] = A[next[i-1]+1] 那麼next[i] = next[i-1],不然,就能夠將模式串繼續前移了。
整個過程是這樣的:
void next_comp(char * str){
   int next[N+1];
   int k = 0;
   next[1] = 0;
   //循環不變性,每次循環的開始,k = next[i-1] 
   for(int i = 2 ; i <= N ; i++){
      //若是當前位置不匹配,或者還推動到字符串開始,則繼續推動
      while(A[k+1] != A[i] && k != 0){
           k = next[k];
      }     
      if(A[k+1] == A[i]) k++;
      next[i] = k;
   } 
}

 


    複雜度分析:從上面的過程能夠看出,內部循環再不斷的執行k = next[k],而這個值必然是在縮小,也就是是沒執行一次k至少減小1;另外一方面k的初值是0,而最多++ N次,而k始終保持非負,很明顯減小的不可能大於增長的那些,因此整個過程的複雜度是O(N)。
    上面是next數組的計算過程,而整個kmp的匹配過程與此相似。

extend-kmp

    爲何叫作擴展-kmp呢,首先咱們看它計算的內容,它是要求出字符串B的後綴與字符串A的最長公共前綴。extend[i]表示B[i...B_len] 與A的最長公共前綴長度,也就是要計算這個數組。觀察這個數組能夠知道,kmp能夠判斷A是不是B的一個子串,而且找到第一個匹配位置?而對於extend[]數組來講,則能夠利用它直接解決匹配問題,只要看extend[]數組元素是否有一個等於len_A便可。顯然這個數組保存了更多更豐富的信息,即B的每一個位置與A的匹配長度。
    計算這個數組extend也採用了於kmp相似的過程。首先也是須要計算字符串A與自身後綴的最長公共前綴長度。咱們設爲next[]數組。固然這裏next數組的含義與kmp裏的有所過程。但它的計算,也是利用了已經計算出來的next[1...i-1]來找到next[i]的大小,總體的思路是同樣的。
    具體是這樣的:觀察下圖能夠發現
    首先在1...i-1,要找到一個k,使得它知足k+next[k]-1最大,也就是說,讓k加上next[k]長度儘可能長。實際上下面的證實過程當中就是利用了每次計算後k+next[k]始終只增不減,而它很明顯有個上界,來證實整個計算過程複雜度是線性的。以下圖所示,假設咱們已經找到這樣的k,而後看怎麼計算next[i]的值。設len = k+next[k]-1(圖中咱們用Ak表明next[k]),分狀況討論:
  • 若是len < i 也就是說,len的長度還未覆蓋到Ai,這樣咱們只要從頭開始比較A[i...n]與A的最長公共前綴便可,這種狀況下很明顯的,每比較一次,必然就會讓i+next[i]-1增長一.
  • 若是len >= i,就是咱們在圖中表達的情形,這時咱們能夠看到i這個位置如今等於i-k+1這個位置的元素,這樣又分兩種狀況:
  1. 若是 L = next[i-k+1] >= len-i+1,也就是說L處在第二條虛線的位置,這樣咱們能夠看到next[i]的大小,至少是len-i+1,而後咱們再今後處開始比較後面的還可否匹配,顯然若是多比較一次,也會讓i+A[i]-1多增長1.
  2. 若是 L < len-i+1 也就是說L處在第一條虛線位置,咱們知道A與Ak在這個位置匹配,但Ak與Ai-k+1在這個位置不匹配,顯然A與與Ai-k+1在這個位置也不會匹配,故next[i]的值就是L。這樣next[i]的值就被計算出來了,從上面的過程當中咱們能夠看到,next[i]要麼能夠直接由k這個位置計算出來,要麼須要在逐個比較,可是若是須要比較,則每次比較會讓k+next[k]-1的最大值加1.而整個過程當中這個值只增不減,並且它有一個很明顯的上界k+next[k]-1 < 2*len_A,可見比較的次數要被限制到這個數值以內,所以總的複雜度將是O(N)的。 

後記

    先說幾件我的私事:一、我的目前還沒有肯定工做,本月月底前往北京;二、11月三、4日去北京 · 國家會議中心參加2011中國移動開發者大會( http://cmdc.csdn.net/ ),說不定在當場便見到正在讀此文的你;三、11月5日中午,中軟同盟會北京分會、河北(保定)分會聚會,期待到時候諸君到來。

     再者,老是有很多朋友要求我推薦幾本有關算法學習的書籍或資料,在此,負責任的推薦以下書籍或資料(排名不分前後):一、算法導論二、編程珠璣;三、編程之美;四、結構之法算法之道blog。五、任何一本數據結構教材。   

     但凡看書沒必要囫圇吞棗,最好是閒或靜下心來,再者,一本書看一遍大都都未必能看懂。如我我的書桌上擺着的一本《深度探索c++對象模型》,經常是看了又淡忘,忘了又看。並且此書能解決你全部有關虛擬繼承,虛擬函數的問題,這是你在網上所能看到或找到的千篇一概的文章或資料所不能相比的。以下面的兩幅來自此書的分別闡述虛擬單一繼承(圖1),虛擬多重繼承(圖2)的圖,一切遁入眼簾,昭然若揭(P152~168):


圖1       虛擬單一繼承

圖2       虛擬多重繼承

    下述代碼是改編自深度探索c++對象模型上的,爲了作些測試,先貼下來,往後再下結論:

// virtual.cpp : 定義控制檯應用程序的入口點。
//#include "stdafx.h"
#include <iostream>
using namespace std;

class Base1
{
 public:
    Base1(){}
    virtual ~Base1(){}
    virtual void speakClearly(){}
    virtual Base1* clone() const{
      // cout<<"it is Base1"<<endl;
      // return ;
  }
protected:
  float data_Base1;
};

class Base2
{
public:
  Base2(){}
  virtual ~Base2(){}
  virtual void mumble(){}
  virtual Base2* clone() const{
    // cout<<"it is Base2"<<endl;
    // return;
  }
protected:
  float data_Base2;
};

class Derived:public Base1,public Base2
{
public:
  Derived(){}
  virtual ~Derived(){}
  virtual Derived* clone() const
  {
    // cout<<"it is Derived"<<endl;
    // return;
  }
protected:
  float data_Dervied;
};

int main()
{
  Base1* p1=new Derived();
  p1->clone();
  delete p1;
  return 0;
}

 

    ok,這些東西本與本文無關,只是剛好看到了,不想卻偏離了主題,扯了這麼多。最後,分享喬布斯的一句話:當你意識到你終將死去,你會放下全部一切。如有任何問題,歡迎不吝賜教。轉載,請註明出處。謝謝。

 

本文參考

    1. 維基百科:Trie樹,後綴樹;
    2. 兔子的算法集中營:後綴樹 http://www.cppblog.com/superKiki/archive/2010/10/29/131786.aspx;
    3. 銀河裏的星星:字符串 http://duanple.blog.163.com/blog/static/709717672009825004092/;
    4. 後綴樹的構造方法-Ukkonen詳解 3xian / 三鮮 in GDUT http://blog.163.com/lazy_p/blog/static/13510721620108139476816/
    5. E.M. McCreight. A space-economical suffix tree construction algorithm. Journal of the ACM, 23:262-272, 1976.
    6. E. Ukkonen. On-line construction of suffix trees. Algorithmica, 14(3):249-260, September 1995.
    7. Mark Nelson. Fast string searching with suffix trees. 1996.
    8. fsdev的專欄:實用算法實現-第8篇後綴樹和後綴數組 [1簡介]
    9. 深度探索c++對象模型 侯捷譯 P152~168。
    10. 結構之法算法之道blog:第三章、尋找最小的k個數海量數據處理面試題集錦與Bit-map詳解
相關文章
相關標籤/搜索