后缀数组

本文详细介绍后缀数组的构造原理及其实现过程,包括倍增算法、后缀数组sa、名次数组rank_和heigh数组的概念及其作用。此外还介绍了如何通过后缀数组进行字符串匹配。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0)后缀数组连续看了几个晚上才弄明白。思想很简单,但是实现的细节和技巧却需要好好推敲。

1)后缀数组,倍增算法(介绍了 后缀数组sa,名次数组rank_,以及heigh数组)

大体介绍一下仅在下面代码中的这三个数组的一些特点,对于理解代码很有帮助。首先约定在此处介绍中,len是字符串实际长度(不补0的情况下):sa[],下标是该后缀的名次(1到len),值是该后缀的首字符在字符串中的位置(0到len-1);rank_[],下标是该后缀首字符在字符串中的位置(0到len-1),值是该后缀的名次(1到len);heigh[],下标是该后缀的名次(1到len),值是sa[i]与sa[i-1]的公共前缀长度,且heigh[1]是第一名次的后缀与第零名次后缀的公共前缀长度,因为第零名次后缀不存在所以heigh[1]也没有意义。

①未注释版

#include <iostream>
#include <string.h>
#include <stdio.h>
using namespace std;
const int maxn=100010;
string fir,sec;
int m=27;
int c[27];
int sa[maxn],rank_[maxn],heigh[maxn],t2[maxn],t1[maxn];
void GetSa(int *r){
    int *x=t1,*y=t2;
    int i;
    int n=fir.size();
    n++;
    for(i=0;i<m;i++) c[i]=0;
    for(i=0;i<n;i++) c[x[i]=r[i]]++;
    for(i=1;i<m;i++) c[i]+=c[i-1];
    for(i=n-1;i>=0;i--) sa[--c[x[i]]]=i;
    for(int k=1;k<n;k<<=1){
        int p=0;
        for(i=n-k;i<n;i++) y[p++]=i; 
        for(i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k;

        for(i=0;i<m;i++) c[i]=0;
        for(i=0;i<n;i++) c[x[y[i]]]++;
        for(i=1;i<m;i++) c[i]+=c[i-1];
        for(i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];

        swap(x,y);
        p=1;x[sa[0]]=0;
        for(i=1;i<n;i++)
            x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?p-1:p++;
        if(p>=n){
            for(int i=1;i<n;i++) rank_[sa[i]]=i;
            break;
        }
        m=p;
    }
}
int Cmp_suffix(string pattern,int p,int n2){
    const char *temp=pattern.c_str();
    const char *temp2=fir.c_str();
    return strncmp(temp,temp2+sa[p],n2);
}
int Find(string pattern){
    int n2=pattern.size();
    int n=fir.size();
    if(Cmp_suffix(pattern,1,n2)<0) {cout<<"1处退出"<<endl;return -1;}
    if(Cmp_suffix(pattern,n,n2)>0) {cout<<"2处退出"<<endl;return -1;}
    int L=1,R=n;
    while(L<=R){
        int M=(L+R)/2;
        int res=Cmp_suffix(pattern,M,n2);
        if(res==0) return M;
        if(res<0) R=M-1;
        else if(res>0) L=M+1;
    }
    cout<<"3处退出"<<endl;
    return -1;
}
void GetHigh(int *r){
    int k=0,i,j;
    int n=fir.size();
    for(i=0;i<n;i++){n;
        j=sa[rank_[i]-1];
        if(k) k--;
        while(r[i+k]==r[j+k]) k++;
        heigh[rank_[i]]=k;
    }
}
int main()
{
    cin>>fir;
    int n=fir.size();
    int r[maxn];
    for(int i=0;i<n;i++) r[i]=fir[i]-'a'+1;
    r[n]=0;
    GetSa(r);

    ///输出后缀:
    for(int i=1;i<=n;i++){
        const char *temp=fir.c_str();
        cout<<temp+sa[i]<<endl;
    }cout<<endl;
    
    ///输出后缀数组:
    for(int i=1;i<=n;i++) cout<<sa[i]<<endl; cout<<endl;
   
    ///输出名次数组
    for(int i=0;i<n;i++) cout<<rank_[i]<<endl; cout<<endl;

    GetHigh(r);

    ///输出heigh数组
    for(int i=1;i<=n;i++) cout<<heigh[i]<<endl; cout<<endl;

    ///输入模板字符串pattern,二分查找在fir字符串中是否存在前缀是pattren 的后缀。(即二分查找并判断该模板字符串pattern是否是fir字符串或者其子串的前缀)
    string pattern;
    cin>>pattern;
    int res=Find(pattern);
    if(res==-1)
        cout<<"Not Found.";
    else{
        int p=res;
        for(int i=sa[p];i<n;i++){
            cout<<fir[i];
        }
        cout<<endl;
    }
    return 0;
}

②注释版

#include <iostream>
#include <string.h>
#include <stdio.h>

using namespace std;
const int maxn=100010;//视情况而定,不用因为y[sa[i]+k]而使y的长度比其他数组如sa、t1等长度更大从而maxn要么不统一适用要么就额外增大, 因为r[n]=0,所以y[sa[i]]==y[sa[i-1]]作为第一限制条件使得&&后面的y[sa[i]+k]==y[sa[i-1]+k]不会越界。

string fir,sec;
int m=27;
int sa[maxn];//后缀数组
int rank_[maxn];//名次数组,但在GetSa中并非真实的rank值,只不过能用它比较各个字符串从而起到rank的作用(如qwert的5个后缀的rank值本应该是2 5 1 3 4,但是在GetSa之后未加任何处理之前得到的rank_数组是,17 23 5 18 20)
int heigh[maxn];//sa[i]与sa[i-1]的最长公共前缀的长度

int c[27];//统计各个字符,出现的次数,我们假设现在的字符串中完全由字母组成,而在代码中带入的下标是,某字符-'a'+1,所以用到的下标是从1到26,长度大于等于26即可
int t2[maxn];
int t1[maxn];

void GetSa(int *r){
    int *x=t1,*y=t2;//
    int i;
    int n=fir.size();
    n++;//因为r[n]==0,所以字符的个数加1
    //对于所有一个字符长度的后缀进行基数排序,并得到第一次的sa[]数组
    for(i=0;i<m;i++) c[i]=0;
    for(i=0;i<n;i++) c[x[i]=r[i]]++;//x[]先存储原字符串 的Asc码,虽然此时不是rank值,但是起到了rank值的作用,即能在单个字符间互相比较大小,从而容易得到顺序
    for(i=1;i<m;i++) c[i]+=c[i-1];//累计是第几个
    for(i=n-1;i>=0;i--) sa[--c[x[i]]]=i;

    for(int k=1;k<n;k<<=1){
        int p=0;
        //利用基数排序求得的sa数组,根据第二关键值(当前值是合并后的第二关键值所以往前移k个假如不越界的话,或者当前值的位置处在太靠后的位置以至于处在该位的合并后的数的第二关键值因为没有所以默认为0),进行排序
        for(i=n-k;i<n;i++) y[p++]=i; //先将没有第二关键值(即默认为0)的数放进y数组(根据第二关键值排序),且所以他们的下标自然是当前位置
        for(i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k;//因为是将当前位置的数当做合并后的数的第二关键值来看,所以他们合并后的位置,自然是-k,自然要先if判断合并后的位置是否越界。

        //根据合并后的值得第一关键值,进行基数排序
        for(i=0;i<m;i++) c[i]=0;
        for(i=0;i<n;i++) c[x[y[i]]]++;//y[i]是合并后的位置,也就是合并前第一个数的位置,所以x[y[i]]就是这个位置上的数的asc码
        for(i=1;i<m;i++) c[i]+=c[i-1];
        for(i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];

        //
        swap(x,y);
        p=1;x[sa[0]]=0;
        for(i=1;i<n;i++)
            x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?p-1:p++;//可能会有名次相同的二元组
        if(p>=n){
            for(int i=1;i<n;i++) rank_[sa[i]]=i;//得到真正的rank值,因为之前r[n]=0,导致sa[0]=n,rank_[n]=0。即sa真正的对于字符串的下标从1开始而sa的值(对应字符串中的位置)从0开始;rank_的下标(对应字符串中的位置)从0开始,而rank_值从1开始。即名次都是从1开始,位置都是从0开始。
            break;
        }//每一个二元组的名次在此时都是唯一的,所以不需再继续倍增比较了
        m=p;
    }
}
int Cmp_suffix(string pattern,int p,int n2){//p 是 后缀的名次,sa[p]是第p名后缀的首字符的位置,那么fir+sa[p]是第p名的后缀字符串
    const char *temp=pattern.c_str();
    const char *temp2=fir.c_str();
    return strncmp(temp,temp2+sa[p],n2);//n2是比较的长度,strncmp(str1,str2,len)比较函数,比较len长度,若str1的ASC码等于str2则返回0,否则返回str1的ASC-str2的ASC
}
int Find(string pattern){
    int n2=pattern.size();//n2是模板的长度
    int n=fir.size();
    if(Cmp_suffix(pattern,1,n2)<0) {cout<<"1处退出"<<endl;return -1;}
    if(Cmp_suffix(pattern,n,n2)>0) {cout<<"2处退出"<<endl;return -1;}
    int L=1,R=n;
    while(L<=R){
        int M=(L+R)/2;
        int res=Cmp_suffix(pattern,M,n2);
        if(res==0) return M;//模板sec是第M名后缀的前缀
        if(res<0) R=M-1;
        else if(res>0) L=M+1;
    }
    cout<<"3处退出"<<endl;
    return -1;
}
void GetHigh(int *r){
    int k=0,i,j;
    int n=fir.size();
    for(i=0;i<n;i++){//这里的n就是字符串的实际长度没有加上最后一个自己加的0,所以<=n   //另外rank的下标虽然也是描述位置,但是是从1开始的。因为赋值时的rank[sa[i]]=i;而sa[0]=n;sa[1]=真正的字符串中第一个(ASC最小)后缀的首字符所在位置
        j=sa[rank_[i]-1];
        if(k) k--;
        while(r[i+k]==r[j+k]) k++;
        heigh[rank_[i]]=k;//极端情况是,rank_[i]==1,那么此时那么j==sa[0]==n,j+k>=n+0,r[n]是自己加的0,因为字母中ASC最小的'a'对应的r数组中的值也是1,所以r[i+k]!=r[n]。只需注意,因为heigh[i]定义是sa[i]与sa[i-1]的公共前缀的长度,所以heigh[0]是没有意义的。
    }
}
int main()
{

    cin>>fir;
    int n=fir.size();
    int r[maxn];
    for(int i=0;i<n;i++){
        r[i]=fir[i]-'a'+1;//从1开始,就可以检验"aaaaa"的字符串了,否则因为a-'a',得到0,排序aaa...aaa会有错
    }
    r[n]=0;//防止越界,用于GetSa中最后一个for循环中的检验数据,相同字母的字符串
    GetSa(r);
    
    ///输出后缀:
    for(int i=1;i<=n;i++){
        const char *temp=fir.c_str();
        cout<<temp+sa[i]<<endl;
    }cout<<endl;

    ///输出后缀数组:
    for(int i=1;i<=n;i++) cout<<sa[i]<<endl; cout<<endl;
    
    ///输出名次数组
     for(int i=0;i<n;i++) cout<<rank_[i]<<endl; cout<<endl;

    GetHigh(r);
    
    ///输出heigh数组
    for(int i=1;i<=n;i++) cout<<heigh[i]<<endl; cout<<endl;

    ///输入模板字符串pattern,二分查找在fir字符串中是否存在前缀是pattren的后缀。(即二分查找并判断该模板字符串pattern是否是fir字符串或者其子串的前缀)
    string pattern;//等待比较的模板,寻找该模板是不是字符串fir的某一个后缀的前缀
    cin>>pattern;
    int res=Find(pattern);
    if(res==-1)
        cout<<"Not Found.";
    else{
        int p=res;
        for(int i=sa[p];i<n;i++){
            cout<<fir[i];
        }
        cout<<endl;
    }
    return 0;
}



资源下载链接为: https://pan.quark.cn/s/22ca96b7bd39 在 IT 领域,文档格式转换是常见需求,尤其在处理多种文件类型时。本文将聚焦于利用 Java 技术栈,尤其是 Apache POI 和 iTextPDF 库,实现 doc、xls(涵盖 Excel 2003 及 Excel 2007+)以及 txt、图片等格式文件向 PDF 的转换,并实现在线浏览功能。 先从 Apache POI 说起,它是一个强大的 Java 库,专注于处理 Microsoft Office 格式文件,比如 doc 和 xls。Apache POI 提供了 HSSF 和 XSSF 两个 API,其中 HSSF 用于读写老版本的 BIFF8 格式(Excel 97-2003),XSSF 则针对新的 XML 格式(Excel 2007+)。这两个 API 均具备读取和写入工作表、单元格、公式、样式等功能。读取 Excel 文件时,可通过创建 HSSFWorkbook 或 XSSFWorkbook 对象来打开相应格式的文件,进而遍历工作簿中的每个 Sheet,获取行和列数据。写入 Excel 文件时,创建新的 Workbook 对象,添加 Sheet、Row 和 Cell,即可构建新 Excel 文件。 再看 iTextPDF,它是一个用于生成和修改 PDF 文档的 Java 库,拥有丰富的 API。创建 PDF 文档时,借助 Document 对象,可定义页面尺寸、边距等属性来定制 PDF 外观。添加内容方面,可使用 Paragraph、List、Table 等元素将文本、列表和表格加入 PDF,图片可通过 Image 类加载插入。iTextPDF 支持多种字体和样式,可设置文本颜色、大小、样式等。此外,iTextPDF 的 TextRenderer 类能将 HTML、
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值