之前发过一篇关于后缀数组的东西,但是不够详细,注释足够详细,但是究其原理可能还是会有所不懂,因此我把我的学习过程讲述一下,首先肯定是要学习基数排序,这个我之前在别人的博客上看到一个例子感觉非常不错,自己也用这个例子反复模拟了很久,其实原理并不难,也很容易懂,但是千万要注意自己一定要手推一遍,不然一下细节问题在你自己默写代码的时候很容易暴露出来。这里给出我学习基数排序的代码(大家可以利用输出中间结果的方式看每一次的结果,我也写了输出的函数,自行调用即可),这个代码是按照由低位到高位依次比较:
//基数排序
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int getdigit(int x,int d){
int a[] = {1, 1, 10, 100};
return (x/a[d])%10;
}
void PrintArr(int a[], int len){
for(int i = 0; i < len; i++){
printf("%d ", a[i]);
}
printf("\n");
}
void jishu_sort(int arr[], int from, int to, int d){
const int radix = 10;
int c[radix];
int *bucket = new int[to-from+1];
for(int k = 1; k <= d; ++k){ //有多少位排几次序
for(int i = 0; i < radix; i++) c[i] = 0; //清0
for(int i = from; i <= to; i++) c[getdigit(arr[i], k)]++; //将该位对应的数字下标的位置增加1.
for(int i = 1; i < radix; i++) c[i] += c[i-1]; //c[i]表示第i个桶的右边界索引
//数据依次入桶
for(int i = to; i >= from; i--){ //从右往左扫描,保证排序稳定性
int j = getdigit(arr[i], k); //求出关键码的第k位数字
bucket[c[j]-1] = arr[i]; //放入对应桶中,c[j]-1是第j个桶的右边界索引。
--c[j]; //对应桶中装入数据索引减一
}
//注意此时c[i]为第i个桶左边界
//从各个桶收集数据
for(int i = from, j = 0; i <= to; i++,j++) arr[i] = bucket[j]; //
}
delete [] bucket;
}
int main(){
int br[10] = {278, 109, 63, 930, 589, 184, 505, 269, 8, 83};
cout << "inital digit" << endl;
PrintArr(br, 10);
jishu_sort(br, 0, 9, 3);
cout << "after sort" << endl;
PrintArr(br, 10);
return 0;
}
其实自己开始理解了半天主要是卡在第二个基数排序上,这个东西老是感到怪怪的(不知道大家是什么感觉),大家需要记得的是虽然是对第二关键字进行排序,但是y数组中一直存放的都是第一个关键字的下标,因此根据第二个关键字对第一个关键字按照重新得到的大小顺序进行存储。又因为得到sa数组的时候我可以发现i是从(n-1)到0的,这里我们会发现相同的字符靠前的小,因此对于这一次基数排序就很明显是第一关键字和第二关键字一起作用的结果。基础排序之后得到了新的sa数组,同样x数组一样也要更新,但是还需要得到新的x数组,这时候我们就利用指针对x和y 2个指针进行交换,这样我们重新计算x,这里面大家会发现p真是用来进行下一次比较大小的数据,如果一个元素的第一关键字和第二关键字跟排在前面的相同,很明显那么x数组存储的值也是相同的,如果不同的话,很明显x数组存储的值比上一步存储的值大,因此p++.
void build_sa(int n,int m){
int i,*x = t, *y = t2; //引用指针只是为了后面好交换
for(i = 0; i < m; i++) c[i] = 0;
for(i = 0; i < n; i++) c[x[i] = s[i]]++;
for(i = 1; i < m; i++) c[i] += c[i-1];
for(i = n-1; i >= 0; i--) sa[--c[x[i]]] = i; //sa[i]中表示排名第i的位置是多少
for(int k = 1; k <= n; k <<= 1){ //k表示每次基数排序需要比较的长度,因为是按照倍增算法所以每次比较2个关键字
int p = 0;
//直接利用sa数组排序第二关键字
for(i = n-k; i < n; i++) y[p++] = i; //y中存放按第二关键字从小到大排序的位置
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 = 0; i < m; i++) c[i] += c[i-1];
for(i = n-1; i >= 0; i--) sa[--c[x[y[i]]]] = y[i]; //i从大到小是为了保证相同字符的情况下默认靠前的更小一些
swap(x, y); //这里只用交换指针即可
p = 1; x[sa[0]] = 0; //p表示rank值不同的字符串的数量,如果达到n表示字符串的所有关系都找出来了
for(i = 1; i < n; i++) //重新计算x的值
x[sa[i]] = y[sa[i-1]] == y[sa[i]] && y[sa[i-1]+k] == y[sa[i]+k]?p-1:p++;
if(p >= n) break;
m = p;
}
}
同样得到height数组同样需要多加思考,对于刘汝佳书上写的我感觉已经非常清楚。height[i]表示sa[i]与sa[i-1]的最长公共前缀。白书上证明了height[rank[i]] >= height[rank[i-1]] -1,因为我们会发现如果第i个位置的前一个位置i-1的height[rank[i-1]],因为第i-1个位置去掉一个首字母得到的正是以i开始的字符串。因为前一个能匹配height[rank[i]],那么后一个最少是去掉i-1个位置去掉首字母之后的长度,也就是height[rank[i-1]] -1,这样一来就很明显了,我们直接利用这个递推公式就可以求出来了。
void getHeight(int n){
int i,j,k = 0;
for(i = 0; i < n; i++) rank[sa[i]] = i; //求出rank值,利用rank和sa是相反的
for(i = 0; i < n; i++){
if(k) k--; //利用h[i] >= h[i-1]+1这个性质,先求出前面的后面的就可以由前面推出
j = sa[rank[i]-1];
while(s[i+k] == s[j+k]) k++;
height[rank[i]] = k;
}
}
自己的后缀数组比较关键部分的理解就到这里了,当然可能有些地方还是说的有些模糊,不过如果没有看懂还是希望在草稿纸上模拟一遍,易于理解还很使用。如果有什么问题也请大家与我交流.