后缀树

后缀树

简介:后缀树,就是把一串字符的所有后缀保存并且压缩的字典树。相对于字典树来说,后缀树并不是针对大量字符串的,而是针对一个或几个字符串来解决问题,

比如字符串的回文子串,两个字符串的最长公共子串等等,后面应用会说。

性质:一个字符串构造了一棵树,树中保存了该字符串所有的后缀。

操作:就是建立和应用。

1.建立后缀树

比如单词banana,它的所有后缀显示到下面的。1代表从第一个字符为起点,终点不用说都是字符串的末尾。

以上面的后缀,我们建立一颗后缀树。如下图,为了方便看到后缀,我没有合并相同的前缀

                                            (3)

基于之前前缀树的学习,可写出插入后缀树代码的编写

void SInsert(TrieNode *root , char *word){//后缀树插入
	int length =strlen(word);
	for(int i=0;i<length;i++){
		char *a=word+i;
        Insert(root, a); 
    }
}
void Insert(TrieNode *root , char *word)  
{  
    TrieNode *location=root;
    while(*word){
        if(location->next[*word-'a']==NULL){
            TrieNode *newNode=new TrieNode();
            newNode->isStr=false;
            memset(newNode->next,NULL,sizeof(newNode->next));//初始化
            location->next[*word-'a']=newNode;  
        }
        location=location->next[*word-'a'];//location指向字符串在前缀树中下一个位置
        word++; //当前字符在字符串中位置
    }
    //字符串已经全部添加到前缀树
    //标识前缀树到该节点位置为完整字符串
    location->isStr=true;
}  


前面简介的时候我们说了,后缀树是把一个字符串所有后缀压缩并保存的字典树。

压缩一会再说,简介里面说了是字典树,所以我们把字符串的所有后缀还是按照字典树的规则建立,就成了上图(3)的样子。

注意还是和字典树一样,根节点必须为空。

下面说下更加节省空间的方案,也就是上面提到的压缩。

                                                                (4)

因为有些后缀串可能是单串,并不和其他的共用同一个前缀。

比如图(4)的banana这个后缀串,直接可以用1来表示起点,终点是默认的。

图(4)的a节点后面有两个节点标记3和5是右边字符数组的下标,对应着a->3-7,a->5-7。因为a是共有的前缀。


2.重点说下后缀树的应用,它能解决大多数字符串的问题

<1.查找某个字符串s1是否在另外一个字符串s2中

这个很简单,如果s1在字符串s2中,那么s1必定是s2中某个后缀串的前缀

理解以下后缀串的前缀这个词,其实每个后缀串也就是起始地点不同而已,前缀也就是从开头开始结尾不定。

后缀串的前缀就可以组合成该原先字符串的任意子串了。

比如banana,anan是anana这个后缀串的前缀。

代码

bool qianzhuiSearch(TrieNode *root , char *word)  
{   
	int length =strlen(word);
    TrieNode *location=root; 
	for(int i=0;i<length;i++){
        if(location->next[*word-'a']==NULL){
			return 0;
		}
		else {
		    location=location->next[*word-'a'];
		    word++;
		}
	}
    return 1;
}


<2.指定字符串s1在字符串s2中重复的次数

看图(3),比如说banana是s1,an是s2,那么计算an出现的次数实际上就是看an是几个后缀串的前缀。

上图的a节点是保存所有起始为a字母的后缀串,我们看a字母后的n字母的引用计数即可。

核心代码

int ncount(TrieNode *location, int &count){
	if(location->isStr==true){         //如果这个点是一个后缀字符串,count++
		count++;}
	for(int i=0;i<26;i++){
	    if(location->next[i]!=NULL){  
		   TrieNode *now=location;   //定义一个now是此代码核心,不能直接用location,因为是它是指针,会影响后续循环递归的过程
		   now=now->next[i];
		   count=ncount(now, count);  //递归
		}
	}
	return count;
}
int countqianzhuiSearch(TrieNode *root , char *word)  
{   
	int length =strlen(word);
    TrieNode *location=root; 
	for(int i=0;i<length;i++){   //找到以“需要计数的字符串”为头后缀树,然后以此向后查找后缀树数,就是重复数
        if(location->next[*word-'a']==NULL){
			return 0;
		}
		else {
		    location=location->next[*word-'a'];
		    word++;
		}
	}
	int count=0;
	TrieNode *now=location;
    count=ncount(now, count);
	return count;
}
在此,列出完整代码

#include<iostream>
#include<string>
using namespace std;
#define MAX 26 //字符集大小

class TrieNode{ 
public:
    bool isStr; //标识是否是一个完整的字符串  
    TrieNode *next[MAX];  
}; 

/*插入*/  
void Insert(TrieNode *root , char *word)  
{  
    TrieNode *location=root;
    while(*word){
        if(location->next[*word-'a']==NULL){
            TrieNode *newNode=new TrieNode();
            newNode->isStr=false;
            memset(newNode->next,NULL,sizeof(newNode->next));//初始化
            location->next[*word-'a']=newNode;  
        }
        location=location->next[*word-'a'];//location指向字符串在前缀树中下一个位置
        word++; //当前字符在字符串中位置
    }

    //字符串已经全部添加到前缀树
    //标识前缀树到该节点位置为完整字符串
    location->isStr=true;
}  

//查找  
bool Search(TrieNode *root , char *word)  
{   
    TrieNode *location=root; 
    while(*word&&location!=NULL){
      location=location->next[*word-'a'];
      word++;
    }
    return(location!=NULL&&location->isStr );
}

//前缀查找
bool qianzhuiSearch(TrieNode *root , char *word)  
{   
	int length =strlen(word);
    TrieNode *location=root; 
	for(int i=0;i<length;i++){
        if(location->next[*word-'a']==NULL){
			return 0;
		}
		else {
		    location=location->next[*word-'a'];
		    word++;
		}
	}
    return 1;
}
int ncount(TrieNode *location, int &count){
	if(location->isStr==true){         //如果这个点是一个后缀字符串,count++
		count++;}
	for(int i=0;i<26;i++){
	    if(location->next[i]!=NULL){  
		   TrieNode *now=location;   //定义一个now是此代码核心,不能直接用location,因为是它是指针,会影响后续循环递归的过程
		   now=now->next[i];
		   count=ncount(now, count);  //递归
		}
	}
	return count;
}
int countqianzhuiSearch(TrieNode *root , char *word)  
{   
	int length =strlen(word);
    TrieNode *location=root; 
	for(int i=0;i<length;i++){   //找到以“需要计数的字符串”为头后缀树,然后以此向后查找后缀树数,就是重复数
        if(location->next[*word-'a']==NULL){
			return 0;
		}
		else {
		    location=location->next[*word-'a'];
		    word++;
		}
	}
	int count=0;
	TrieNode *now=location;
    count=ncount(now, count);
	return count;
}
void Delete(TrieNode *location){
    for(int i=0;i<MAX;i++){
        if(location->next[i]!=NULL){
            Delete(location->next[i]);
        }
    }
    delete location;    
}

void SInsert(TrieNode *root , char *word){//后缀树插入
	int length =strlen(word);
	for(int i=0;i<length;i++){
		char *a=word+i;
        Insert(root, a); 
    }
}
int main(){
    //初始化前缀树的根节点,注意这里结构体指针的初始化
    TrieNode *root=new TrieNode();
    root->isStr =false;

    //前缀树中每一个节点的下一个节点,分配空间,注意memset的使用
    memset(root->next,NULL,sizeof(root->next));

    SInsert(root,"banananana");//后缀树插入
    //Insert(root,"bcd");
    //Insert(root,"xyz");
    //Insert(root,"abcdef");

    //if(qianzhuiSearch(root,"bcf"))//用来查找某个字符串s1是否在另外一个字符串s2
    //    cout<<"exist"<<endl;
    //else
    //    cout<<"no exist"<<endl;
	int z=countqianzhuiSearch(root,"ban");
	cout<<z;
    Delete(root);
}


先说下广义后缀树,前面说了后缀树可以存储一个或多个字符串,当存储的字符串数量大于等于2时就叫做广义后缀树。

<3.两个字符串S1,S2的最长公共部分(广义后缀树)

建立一棵广义后缀树,如下图(5)

                                                                             (5)

$和#是为了区分字符串的。

(上面的程序不能实现这个代码,因为isStr是bool型,若想实现,改成int型即可,第一个字符串标识为1,第二个字符串标识为2等) 

我们为每个后缀串末尾单独添加一个空间存储区分字符串的符号。

那么怎么找s1和s2串最长的公共部分?

遍历每个后缀串,如果其引用计数为1则直接跳过,因为不可能有两个子串存放在这里,当引用计数>1时,往下遍历,直到分叉分别记录子串的符号,

如果不同,说明他们是不同字符串的,记录已经匹配的值即可,若相同继续下一次遍历。

上图的ana部分,到ana时,子串$结束,然后继续向下,子串anab以#结束,那么匹配了ana。


<4.最长回文串(广义后缀树)

把要求的最长回文串的字符串s1和它的反向(逆)字符串s2建立一棵广义后缀树。


回文串有一个定义就是正反相同,也就是正着和反着可以重和在一起,那么我们直接看这棵广义后缀树的共同前缀即可,每个banana的子串和ananab的子串重合的部分

都是回文串,我们只需要找到最长的即可。比如上面的anana,从后面不同的标记可以看出两个字符串的某个后缀都有这个前缀,能完美重合到一起。即它是回文串。

记录Max,每次找到一个回文串比较即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值