数组sa[i]就表示保存的是S字符串的全部后缀在以字典序排序后,排在第i名的字符串在原来子串中的位置,如ABAB在字典序中为3,但在后缀数组中排名为2,因此sa(3)=2;
映射数组rk[i]就表示S字符串的全部后缀在以字典序排序后,原来的第i名如今排第几,如BAB在后缀中排名为3,但字典序中排名为5,rank(3)=5.
字符串的字典序排列为:
这个表明了后缀数组的性质
sa(rank(i))=rank(sa(i))=i
sa(rank(1))=1
sa(rank(2))=2
这表明后缀数组和排名数组是一个互相的映射关系。
暴力比较法
对于字典序后缀数组的排序,使用暴力解法时,排序的复杂度为O(nlogn),而对每一个元素进行两两比较时,复杂度为n,总体的复杂度为O(n2logn).
倍增法
class Solution {
public static boolean getRank(int[] nums, int step){
//假设输入nums = [1,1,2,1,1,1,1,2],step = 1,则相当于对以下数字对进行排序:<1,1>,<1,2>,<2,1>,<1,1>,<1,1>,<1,1>,<1,2>,<2,0>
int length = nums.length;
//result1保存低位基数排序的结果
int[] result1 = new int[length+1];
//result2保存高位基数排序的结果
int[] result2 = new int[length+1];
//count用来统计一趟基数排序中各个数位出现的次数,由于nums里面数值范围是从1到26,所以count长度不能小于26。
//且在第二次进入该函数时,nums数组里面放的是当前阶段各子串的排名,到最后有length个子串,即有length个排名值,所以count的长度也不能低于length
int[] count = new int[Math.max(27, nums.length)];
//低位基数排序,低位数组 = [1,2,1,1,1,1,2,0], 完成后result1 = [7,0,2,3,4,5,1,6]
for(int i = 0; i < length; i++){
int digit = (i+step)<length?nums[i+step]:0;
count[digit]++;
}
for(int i = 1; i < count.length; i++){
count[i] += count[i-1];
}
for(int i = length-1; i >= 0; i--){
int digit = (i+step)<length?nums[i+step]:0;
result1[count[digit]-1] = i;
count[digit]--;
}
//高位基数排序,高位数组 = [1,1,2,1,1,1,1,2], 完成后result2 = [0,3,4,5,1,6,7,2]
count = new int[Math.max(27, nums.length)];
for(int i = 0; i < length; i++){
int digit = nums[i];
count[digit]++;
}
for(int i = 1; i < count.length; i++){
count[i] += count[i-1];
}
for(int i = length-1; i >=0;i--){
int digit = nums[result1[i]];
result2[count[digit]-1] = result1[i];
count[digit]--;
}
//result2前4个值为0,3,4,5,分别代表起始下标为0,3,4,5的4个<1,1>,这些只能占用一个排名号,即1
//根据result2里面的基数排序结果,重新编排,使相等的元素只占用一个排名,结果保存到result1,完成后result1 = [1,2,4,1,1,1,2,3]
result1[result2[0]] = 1;
for(int i = 1; i < length; i++){
int index1 = result2[i];
int index2 = result2[i-1];
if(nums[index1] == nums[index2] && ((index1+step)<length?nums[index1+step]:0) == ((index2+step)<length?nums[index2+step]:0)){
result1[index1] = result1[index2];
}else{
result1[index1] = result1[index2]+1;
}
}
//结果复制到nums里面,便于进行下一轮
System.arraycopy(result1, 0, nums, 0, nums.length);
return result1[result2[length-1]]==length;
}
public static int[] getHeight(int[] nums, int[] rank, int[] sa){
int length = nums.length;
int[] height = new int[length];
//step表明前一趟比较的结果,可以使得这一趟比较跳过前面若干位
int step = 0;
for(int i = 0; i < length; i++){
//若当前后缀是排名第1的,则无法计算height,跳过
if(rank[i] == 1){
step = 0;
continue;
}
//index1和index2分别表示suffix(i)和它前一名的后缀的起始比较位置
int index1 = i+step;
int index2 = sa[rank[i]-2] - 1+step;
for(; index1<length && index2<length;){
if(nums[index1] == nums[index2]){
index2++;
index1++;
}else{
break;
}
}
height[rank[i]-1] = index1-i;
step = Math.max(0, index1-i-1);
}
return height;
}
public String longestDupSubstring(String s) {
//初始操作,新建各种数组等
int length = s.length();
int[] nums = new int[length];
int[] rank = new int[length];
for(int i = 0; i < length; i++){
nums[i] = s.charAt(i)-'a'+1;
rank[i] = s.charAt(i)-'a'+1;
}
//求rank数组,isOk检查当前rank数组是否已经收敛
boolean isOk = false;
for(int step = 1; step/2 < length && !isOk; step*=2){
isOk = getRank(rank, step);
}
//逆操作从rank求sa
int[] sa = new int[length];
for(int i = 0; i < length; i++){
sa[rank[i]-1] = i+1;
}
//rank和sa结合求height
int[] height = getHeight(nums, rank, sa);
//遍历height求最长重复子串
int max = 1;
String result = "";
for(int i = 0; i < length; i++){
if(height[i] >= max){
max = height[i];
result = s.substring(sa[i]-1, sa[i]-1+height[i]);
}
}
return result;
}
}
后缀数组的模板
要明白的最重要的一点其实是height[]数组的含义,其含义是sa[i]和sa[i-1]的最长公共前缀。
height[i] 的值等于 suffix(sa[i-1]) 和 suffix(sa[i])的最长公共前缀
sa[i]上的字符串组实际上是按照字典序进行的排列,即排名第i的后缀和排名第i-1的后缀的
最长公共前缀,排名指的是字典序的排名。
在height[]数组的求解的过程中,并不直接的求解height[]数组,这里是通过引入了一个中间的结
果h[]数组,其中h[]数组中的数值通过height[]数组进行的填充
h[i] = height[rank[i]],其实也就是字典序的数组的,依次进行公共前缀的求取
这里实际上还有一个简化的过程:例如求解
求h[1]要比对:
aabaaaab --sa[rank[1]]
aab --sa[rank[1] - 1]
其公共的前缀就是aab,而在求取h[2]的时候呢
abaaaab --sa[rank[2]]
ab --sa[rank[2] - 1]
实际上的公共前缀是ab,而在这里的公共前缀的求取上可以通过h[1]的公共前缀的求解上简化
已知h[1]的公共前缀是aab。而在移动了一位以后实际上h[1]的公共前缀是aab->ab,此时的
情况是h[2]的求解应该是从第三位开始的,所以其公共的前缀的计算可以从第三位开始计算,其复杂度
变成了O(n).