後綴樹----構建
1.後綴樹簡介2.算法設計3.模塊描述(1)數據類型(2)算法描述
1.後綴樹簡介
後綴樹是一種數據結構,一個具有m個字符的字符串S的後綴樹T,就是一個包含一個根節點的有向樹,該樹恰好帶有m+1個葉子,這些葉子被賦予從0到m的標號。每一個內部節點,除了根節點以外,都至少有兩個子節點,而且每條邊都用S的一個子串來標識。出自同一節點的任意兩條邊的標識不會以相同的字符開始。後綴樹的關鍵特徵是:對於任何葉子i,從根節點到該葉子所經歷的邊的所有標識串連起來後恰好拼出S 的從i位置開始的後綴,即S[I,…,m]。(這裏有一個規定,即字符串中不能有空格,且最後一個字符不能與前面任何一個字符相同)
爲了方便理解概念,給出一個例子,下圖是字符串"banana#"的後綴樹。
2.算法設計
源代碼參見我的Github。
本文章利用的是Ukkonen算法構建後綴樹,在 1995 年,Esko Ukkonen 發表了論文《On-line construction of suffix trees》,描述了在線性時間內構建後綴樹的方法。 本文章參考數據結構系列——後綴樹(附Java實現代碼),理解之後做了適當的改進並用c++語言實現,代碼在dev-cpp 5.11已測試。
首先解釋一下將要用到的幾個概念:活動點(包括活動節點,活動邊,活動長度),剩餘後綴數。活動點中的活動節點:是用於查找一個後綴是否已經存在這棵樹裏,即查找的時候從活動節點的子節點開始查找,同時當需要插入邊的時候也是插入到該節點下;而活動邊則是每次需要進行分割的邊,即成爲活動邊就意味着需要被分割;而活動長度則是指明從活動邊的哪個位置開始分割。剩餘後綴數是我們需要插入的後綴的數量,說明程序員點就是緩存的數量,因爲每次如果要插入的後綴存在,則緩存起來。另外還用到了後綴字符數組,表明將要處理的後綴字符。
對於指定的字符串,從前往後一次提取一個字符,將其加入後綴字符數組,然後剩餘後綴數加一,在當前後綴樹中(當然,剛開始樹是空的)尋找是否存在當前字符的後綴,如果有,則繼續進行循環讀取下一個字符,如果沒有,則進入後綴字符處理函數進行後綴處理,在後綴處理函數中,首先需要定位活動節點,然後在依據活動節點來進行不同的操作。
那麼,先來了解一下幾個規則:
規則一(活動節點爲根節點時候的插入):
o插入葉子之後,活動節點依舊爲根節點;
o活動邊更新爲我們接下來要更新的後綴的首字母;
o活動長度減1;
規則二(後綴鏈表):
o每個階段,當我們建立新的內部節點並且不是該階段第一次建立內部節點的時候, 我們需要用指針從當前內部節點指向本階段最近一次建立的內部節點。
規則三(活動節點不爲根節點時候的插入):
o如果當前活動節點不是根節點,那麼我們每次從活動節點新建一個葉子之後,就要沿着後綴鏈表到達新的節點,並更新活動節點,如果不存在後綴鏈表,我們就轉移到根節點,將活活動節點更新爲根節點但活動長度以及活動邊不變。
額外規則(活動點的晉升)
o如果活動邊上的所有字符全部都被匹配完了(即活動邊上的字符數==活動長度),則將活動邊連接的下一個節點晉升爲活動節點,同時重置活動長度爲0。
也就是說更新活動點後,如果活動節點是根節點則按照規則一進行處理,如果活動節點不是根節點,則按照規則三進行處理,在處理過程中,還要時刻注意規則二和額外規則。當新建節點時,遵循以下規則,如果新建時,活動邊存在,則分裂活動邊,分割的位置由活動長度指定;如果活動邊不存在,則就在活動節點下新建節點和邊。
3.模塊描述
(1)數據類型
首先定義結構變量及類,包括Node結構體,Edge結構體,ActivePoint結構體,以及SuffixTree類。
Node結構體----後綴樹中的節點
struct Node { int flag; int count;//鏈接的邊的個數,用下邊的邊指針數組存儲 Edge *child[max]; Edge *parent; Node *next;//後綴鏈接標識 Node(){flag=-1;parent=NULL;count=0;next=NULL;} Node(int f){flag=f;parent=NULL;count=0;next=NULL;} };
Edge結構體----後綴樹中的邊
struct Edge { string str; Node *above,*below;//head-->above back--->below Edge(){str="";above=NULL;below=NULL;} Edge(Node *above,Node *below,string str) { this->str=str; this->above=above; this->below=below; this->above->child[above->count++]=this; this->below->parent=this; } Edge(Node *above,int i,Node *below,string str) { this->str=str; this->above=above; this->below=below; this->above->child[i]=this; this->below->parent=this; } };
ActivePoint結構體----活動點
struct ActivePoint { Node *node;//活動節點 Edge *edge;//活動邊 int length;//活動長度 ActivePoint(){node=NULL;edge=NULL;length=0;} ActivePoint(Node*n,Edge*e,int len){node=n;edge=e;length=len;} };
SuffixTree類----後綴樹類
class SuffixTree { public: SuffixTree() { root=new Node(); activepoint=new ActivePoint(root,NULL,0); reminder=0; helpstr=""; suffixarray=""; active=NULL; } ~SuffixTree(){delall(root);} //析構函數 void delall(Node *p);//實際釋放空間函數,釋放節點p的所有孩子 (從後往前) int getlength(Node *p);//從p節點向上到根節點經歷過的邊的字符個數 string getstr(Node *node);//從根節點向下到p節點拼出字符串 string getallstr(){return helpstr;}//返回該樹的字符串 bool search(Node *p,string str,Node *&cur);//從p節點向下尋找與字符串str匹配的,找到返回true bool findstr(string str);//查找字符串是否存在 string findlongeststr();//尋找最長重複字符串 void finddeepestr(Node *a[],Node *p,int &cal);//尋找每個分支的最長重複字符串 int count(string str);//計算字符串str出現的次數 int countleaf(Node *p);//計算p節點下的葉節點個數 bool judgeleaf(Node *p);//判斷p節點先是否全爲葉節點 int find(char x);//查找指定的後綴是否存在 void build(string str);//構建後綴樹 void deal(string str,int currentindex);//處理後綴函數 void showtree(){show(root,0,0);}//打印後綴樹 void show(Node *p,int repeat,int len);//打印後綴樹實際函數 void test()//測試用函數,展示當前活動點,後綴字符,剩餘後綴數等信息 { if(activepoint->edge!=NULL) { cout<<"\n apnode="<<getstr(activepoint->node)<<",apedge="<<activepoint->edge->str<<",aplen="<<activepoint->length; cout<<",reminder="<<reminder<<",suffixarray="<<suffixarray<<"\n"; } else { cout<<"\n apnode="<<getstr(activepoint->node)<<",apedge=NULL,aplen="<<activepoint->length; cout<<",reminder="<<reminder<<",suffixarray="<<suffixarray<<"\n"; } } private: Node *root; ActivePoint *activepoint; int reminder; string suffixarray; Node *active; string helpstr; };
(2)算法描述
build(String word):在SuffixTree中定義一個build(String word)方法,是後綴樹構建的入口函數。首先依次提取字符串的每個字符,並按照算法步驟逐個插入。find(char w)用於查找指定的後綴是否存在(這裏所說的後綴其實就是單個字符,因爲單個字符代表的就是以該字符開頭的後綴)。如果當前後綴未找到,就進入後綴字符處理函數deal(),如果找到,就繼續循環。build()源代碼如下,
/**************************************************************************** **build(string str)方法: **以str構造後綴樹 ****************************************************************************/ void SuffixTree::build(string str) { helpstr=str; int index=0; Edge *&apedge=activepoint->edge; Node *&apnode=activepoint->node; int &aplen=activepoint->length; while(index<str.length()) { //cout<<"\n當前處理的: "<<index<<","<<str[index]<<"\n"; //test();cout<<"\n" ; int currentindex=index++; char w=str[currentindex]; //查找是否存在保存有當前後綴字符的節點 if(find(w)!=-1)//如果找到了 { suffixarray+=w; reminder++; continue; } else //如果未找到 { suffixarray+=w; reminder++; } active=NULL; deal(str,currentindex); } }
find():查找後綴是否存在是從活動邊開始查找,如果活動邊爲NULL,則從活動節點的子節點挨個查找,查找是通過比較邊上的指定位置(活動長度指定)與查找字符是否相等。這裏有個地方需要注意:算法中提到,如果一個活動邊已到達結尾(即活動長度==活動邊的字符長度),則將活動邊晉升爲活動節點,並重置活動邊和活動長度爲NULL和0。
/**************************************************************************** **int find(char x)方法: **查找當前後綴是否存在,不存在返回-1 ****************************************************************************/ int SuffixTree::find(char x) { Edge *&apedge=activepoint->edge; Node *&apnode=activepoint->node; int &aplen=activepoint->length; if(apedge==NULL) {//無活動邊,則從活動節點的子節點開始查找 for(int i=0;i<apnode->count;i++) { //cout<<i; Edge *tempedge=apnode->child[i]; if(tempedge->str[0]==x) { aplen++; apedge=apnode->child[i]; if(aplen==apedge->str.length()) {//這裏出現了一個修改活動點的規則:即如果活動邊上的所有字符全部都被匹配完了 //(級活動邊上的字符數==活動長度),則將活動邊晉升爲活動點,同時重置活動長度爲0。 //所以下次查找時就得從該節點開始了,而不是根節點了。 apnode=apedge->below; aplen=0; apedge=NULL; //return 1; } return i; } } return -1; } else {// 有活動邊,則在活動邊上查找 if(apedge->str[aplen]==x) { aplen++; if(aplen==apedge->str.length()) {//這裏出現了一個修改活動點的規則:即如果活動邊上的所有字符全部都被匹配完了 //(級活動邊上的字符數==活動長度),則將活動邊晉升爲活動點,同時重置活動長度爲0。 //所以下次查找時就得從該節點開始了,而不是根節點了。 apnode=apedge->below; aplen=0; apedge=NULL; } return 1; } else return -1; } return -1; }
deal():該方法是用來處理後綴字符的,也是後綴樹構建中的主要部分,主要就是依據上文提到的幾個規則來進行,deal()源代碼如下,
/**************************************************************************** **deal(string str,int currentindex,int number)方法: **處理後綴字符,str是輸入的字符,currentindex是處理到的位置,number表示本次操作 **使用了幾次後綴鏈表 ****************************************************************************/ void SuffixTree::deal(string str,int currentindex) { //cout<<"\n----------------------------------------------\n"; //cout<<"deal函數入口:\n"; // test();show(root,0,0); Edge *&apedge=activepoint->edge; Node *&apnode=activepoint->node; int &aplen=activepoint->length; if(reminder==1)//如果剩餘後綴數爲1 //pay attention to//是否一定爲根,當reminder爲1的時候 { if(apnode==root)//如果活動節點是根節點 {//新建節點 Node *tempnode1=new Node(currentindex-suffixarray.length()+1); Edge *tempedge1=new Edge(apnode,tempnode1,str.substr(currentindex)); suffixarray.erase(0,1); reminder--; apedge=NULL; //cout<<"deal函數出口:\n"; //test();show(root,0,0); //cout<<"\n----------------------------------------------\n"; return; } else//如果活動節點不是根節點,apnode!=root { } } else//剩餘後綴數大於1 { if(apnode==root) { //規則一(活躍點爲根節點時候的插入): //o插入葉子之後,活躍節點依舊爲根節點; //o活躍邊更新爲我們接下來要更新的後綴的首字母; //o活躍半徑減1; if(apedge==NULL)//如果活動邊不存在,即說明活動節點下需要新創建節點 { Node *tempnode1=new Node(currentindex); Edge *tempedge1=new Edge(apnode,tempnode1,str.substr(currentindex)); //活動邊依舊設置爲空 } else { Edge *edge=apedge;//保存當前活動邊,也便於後邊釋放舊有的活動邊 apedge=NULL; aplen--;//因爲一定能找到,因此尋找過程中會使得aplen++,此處修正 int m=find(edge->str[0]);//尋找標號,後邊新建節點會用到 Node *tempnode1=new Node(); Edge *tempedge1=new Edge(tempnode1,apedge->below,apedge->str.substr(aplen)); Edge *tempedge2=new Edge(apnode,m,tempnode1,apedge->str.substr(0,aplen)); Node *tempnode2=new Node(currentindex-suffixarray.length()+1); Edge *tempedge3=new Edge(tempnode1,tempnode2,str.substr(currentindex)); apedge=apnode->child[m]; delete edge;//釋放舊有的活動邊 } //規則二(後綴鏈表): //o每個階段,當我們建立新的內部節點並且不是該階段第一次建立內部節點的時候, //我們需要用指針從當前內部節點指向本階段最近一次建立的內部節點。 //如果當前新建節點是內部節點,則更新後綴鏈表 if(apedge!=NULL&&apedge->below->count>1) { if(active==NULL) active=apedge->below; else { active->next=apedge->below; active=apedge->below; } } else if(apedge==NULL) { if(active==NULL) active=apnode; else { active->next=apnode; active=apnode; } } suffixarray.erase(0,1); reminder--; aplen--; apedge=NULL;//apnode已經爲空 aplen=0; int flag; for(int i=0;i<reminder;i++) { flag=find(suffixarray[i]); } if(flag==-1) { //cout<<"deal函數出口:\n"; //test();show(root,0,0); //cout<<"\n----------------------------------------------\n"; deal(str,currentindex); return; } else { //cout<<"deal函數出口:\n"; //test();show(root,0,0); //cout<<"\n----------------------------------------------\n"; return; } } else//apnode!=root { //規則三(活躍點不爲根節點時候的插入): //o如果當前活躍點不是根節點,那麼我們每次從活躍點新建一個葉子之後, //就要沿着後綴鏈表到達新的節點,並更新活躍節點,如果不存在後綴鏈表, //我們就轉移到根節點,將活躍節點更新爲根節點但活躍半徑以及活躍邊不變。 char temp; if(apedge==NULL)//如果活動邊不存在,即說明活動節點下需要新創建節點 { Node *tempnode1=new Node(currentindex-suffixarray.length()+1); Edge *tempedge1=new Edge(apnode,tempnode1,str.substr(currentindex)); //這個時候活動節點怎麼定義???? //依舊當做新建的內部節點處理 } else { Edge *edge=apedge; temp=edge->str[0]; apedge=NULL; aplen--; int m=find(edge->str[0]); Node *tempnode1=new Node(); //cout<<"what happened?\n";//這裏曾經出現一個問題就是前面的"aplen--"與"int m=find(edge->str[0])"順序錯誤而產生的問題 //cout<<apedge->str<<" "<<aplen;//當順序反後,活動點可能會錯誤的升級,而這不是我想要的 //cout<<apedge->str.substr(aplen)<<"\n"; Edge *tempedge1=new Edge(tempnode1,apedge->below,apedge->str.substr(aplen)); Edge *tempedge2=new Edge(apnode,m,tempnode1,apedge->str.substr(0,aplen)); Node *tempnode2=new Node(currentindex-suffixarray.length()+1); Edge *tempedge3=new Edge(tempnode1,tempnode2,str.substr(currentindex)); apedge=apnode->child[m]; delete edge; } reminder--; suffixarray.erase(0,1); //如果當前新建節點是內部節點,則更新後綴鏈表 if(apedge!=NULL&&apedge->below->count>1) { if(active==NULL)//注意加判定以判斷是否爲內部節點!!! active=apedge->below; else { active->next=apedge->below; active=apedge->below; } } else { if(active==NULL)//注意加判定以判斷是否爲內部節點!!! active=apnode; else { active->next=apnode; active=apnode; } } //開始沿着後綴鏈表尋找,並且重置活動點 if(apnode->next!=NULL)//如果有連接,就進入 { apnode=apnode->next; apedge=NULL; int tempaplen=aplen; aplen=0; int flag; for(int i=reminder-tempaplen-1;i<reminder;i++) { flag=find(suffixarray[i]); } if(flag==-1) { //cout<<"deal函數出口:\n"; //test();show(root,0,0); //cout<<"\n----------------------------------------------\n"; deal(str,currentindex); return; } else { //cout<<"deal函數出口:\n"; //test();show(root,0,0); //cout<<"\n----------------------------------------------\n"; return; } } else//如果當前節點無連接,就置活動節點爲根節點 { apnode=root; apedge=NULL; aplen=0; int flag; for(int i=0;i<reminder;i++) { flag=find(suffixarray[i]); } if(flag==-1) { //cout<<"deal函數出口:\n"; //test();show(root,0,0); //cout<<"\n----------------------------------------------\n"; deal(str,currentindex); return; } else { //cout<<"deal函數出口:\n"; //test();show(root,0,0); //cout<<"\n----------------------------------------------\n"; return; } } }//apnode!=root終結 }//reminder>1終結 }