后缀数组相关问题的记录

文章介绍了后缀数组的性质,包括其与排名数组之间的映射关系,以及如何通过暴力比较和倍增法进行后缀数组的构造。倍增法中详细展示了基数排序的过程,用于优化比较效率。最后,文章提出了求解高度数组(height[])来找到最长重复子串的方法。
摘要由CSDN通过智能技术生成

数组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).

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值