逐步解析力扣1044. 最长重复子串(RK算法)

https://leetcode-cn.com/problems/longest-duplicate-substring/
题目:
给你一个字符串 s ,考虑其所有 重复子串 :即,s 的连续子串,在 s 中出现 2 次或更多次。这些出现之间可能存在重叠。返回 任意一个 可能具有最长长度的重复子串。如果 s 不含重复子串,那么答案为 “” 。

示例 1:
输入:s = "banana"
输出:"ana"

示例 2:
输入:s = "abcd"
输出:""

理解

力扣上面写的例子太简单,一开始没读懂题,先从理解开始,字符串从头开始遍历,然后返回后面重复的字符串,比如“cabcbac”返回c,c是第一位,后面无规律,所以后面只有c是最小子串,“cabcabac”返回cab,从头开始遍历cab,后面有重复,所以cab是最小子串,再给字符串最前面加个a,“acabcabac”还是返回cab而不是ac,因为离cab最近的还是cab,再给前面的基础上再加个c,“acabacabac”返回acabac

重复子串如图所示:
在这里插入图片描述
在这里插入图片描述

解题

读懂题目后,就再看一下官方的解答
在这里插入图片描述
数学基础差,难度很高,一上来肯定会看不懂
这里介绍了一个概念Rabin-Karp 字符串编码,所以要先百度下RK算法,理解个大概,其他解法如DC3算法等先打个眼熟,之后在优化的时候可以进阶学习一下

RK 算法

一般字符串匹配的时候,如果使用暴力匹配算法
在这里插入图片描述

我们假设主串长度为n,模式串长度m,暴力匹配的时间复杂度为O(n*m)。当主串特别长的时候,匹配效率不够高。

哈希算法的应用

我们知道,哈希算法是可以将任意数据转换成一组固定长度的二进制串,所以,我们可以将主串中所有与模式串长度相同的子串的哈希值计算出来,匹配时,首先比较模式串与子串的哈希值。

考虑存在哈希冲突的情况,在匹配到相同哈希值的子串后,还需要进行字符比较,我们将大量的匹配交给了哈希值比较,而计算哈希值和哈希值的比较都是很高效的,因此,效率大大高于暴力匹配法。
在这里插入图片描述
哈希算法的设计

假设我们要匹配的主串只包含[a-z]的小写字母,可以类比十进制数的表示方法,将这些小写字母转换成 26 进制。

在这里插入图片描述

在计算哈希值时,可以通过前一个子串简单计算得到当前子串的哈希值:

在这里插入图片描述
公式:

h[i] = 26 * (h[i-1] - 26^(m-1) * (s[i-1]-'a')) + (s[i+m-1]-'a');

其中, h[i]、h[i-1] 分别对应 s[i] 和 s[i-1] 两个子串的哈希值

注:这里的公式看起来复杂,但是是算法代码里很关键的实现,记不住没关系但一定要眼熟

时间复杂度分析

假设主串长度为n,模式串长度为m

RK 算法匹配时,分为两步:

  1. 计算n-m+1个子串的哈希值和模式串的哈希值
  2. 遍历比较子串哈希值和模式串哈希值

第一步中,时间复杂度为 O(n)
第二步中,哈希值之间的比较时间复杂度为 O(1),因此比较 n-m+1 次,时间复杂度为 O(n)

综上:RK 算法整体时间复杂度为O(n)

光看没用,还要画一下,就会很好理解了
在这里插入图片描述

注意要点:

  1. 当模式串长,计算得到的哈希值超过整数存储范围时,如何处理?
    前文中,我们设计的哈希算法是不存在哈希冲突的,因此,可以允许存在哈希冲突,从而减小哈希值的范围。
    例如,可以将字符串对应值直接相加:bcd ==> 1+2+3=6,这必定会导致严重散列冲突。可以采用其他方法,以达到效率和冲突的平衡。
  2. 存在散列冲突时,如何处理?
    当匹配到哈希值相同的子串时,由于存在散列冲突,因此不能保证该子串与模式串完全一样,我们需要进一步比较子串与模式串的字符,从而确定是否匹配成功。

代码

之后在本地起一下代码,然后打断点一步步理解

/**
 * @author: lzq
 * @description: Algr
 * @date: 2021/12/23 4:37 下午
 */
public class Algr {
    public static void main(String[] args) {
        String a = longestDupSubstring("cabcbac");
        System.out.println(a);
    }
        public static String longestDupSubstring(String s) {
            Random random = new Random();
            // 生成两个进制
            int a1 = random.nextInt(75) + 26;
            int a2 = random.nextInt(75) + 26;
            // 生成两个模
            int mod1 = random.nextInt(Integer.MAX_VALUE - 1000000007 + 1) + 1000000007;
            int mod2 = random.nextInt(Integer.MAX_VALUE - 1000000007 + 1) + 1000000007;
            int n = s.length();
            // 先对所有字符进行编码
            int[] arr = new int[n];
            for (int i = 0; i < n; ++i) {
                arr[i] = s.charAt(i) - 'a';
            }
            // 二分查找的范围是[1, n-1]
            int l = 1, r = n - 1;
            int length = 0, start = -1;
            while (l <= r) {
                int m = l + (r - l + 1) / 2;
                int idx = check(arr, m, a1, a2, mod1, mod2);
                if (idx != -1) {
                    // 有重复子串,移动左边界
                    l = m + 1;
                    length = m;
                    start = idx;
                } else {
                    // 无重复子串,移动右边界
                    r = m - 1;
                }
            }
            return start != -1 ? s.substring(start, start + length) : "";
        }

        public static int check(int[] arr, int m, int a1, int a2, int mod1, int mod2) {
            int n = arr.length;
            long aL1 = pow(a1, m, mod1);
            long aL2 = pow(a2, m, mod2);
            long h1 = 0, h2 = 0;
            for (int i = 0; i < m; ++i) {
                h1 = (h1 * a1 % mod1 + arr[i]) % mod1;
                h2 = (h2 * a2 % mod2 + arr[i]) % mod2;
                if (h1 < 0) {
                    h1 += mod1;
                }
                if (h2 < 0) {
                    h2 += mod2;
                }
            }
            // 存储一个编码组合是否出现过
            Set<Long> seen = new HashSet<Long>();
            seen.add(h1 * mod2 + h2);
            for (int start = 1; start <= n - m; ++start) {
                h1 = (h1 * a1 % mod1 - arr[start - 1] * aL1 % mod1 + arr[start + m - 1]) % mod1;
                h2 = (h2 * a2 % mod2 - arr[start - 1] * aL2 % mod2 + arr[start + m - 1]) % mod2;
                if (h1 < 0) {
                    h1 += mod1;
                }
                if (h2 < 0) {
                    h2 += mod2;
                }

                long num = h1 * mod2 + h2;
                // 如果重复,则返回重复串的起点
                if (!seen.add(num)) {
                    return start;
                }
            }
            // 没有重复,则返回-1
            return -1;
        }

        public static long pow(int a, int m, int mod) {
            long ans = 1;
            long contribute = a;
            while (m > 0) {
                if (m % 2 == 1) {
                    ans = ans * contribute % mod;
                    if (ans < 0) {
                        ans += mod;
                    }
                }
                contribute = contribute * contribute % mod;
                if (contribute < 0) {
                    contribute += mod;
                }
                m /= 2;
            }
            return ans;
        }


}

按照顺序,先生成必要的几个参数
在这里插入图片描述
然后开始编码,遍历-’a‘生成arr的编码数组
在这里插入图片描述
这里进入二分查找循环,left和right取中间m,l和r的范围是[1, n-1]
在这里插入图片描述
idx经过check方法处理,返回根据是否是-1判断重复子串,有重复子串,移动左边界,无重复子串,移动右边界,也就是例子里是[1,6],有重复下次遍历[5,6],无重复下次遍历[1,3]
在这里插入图片描述
遍历完成后,截取的字符串则是[不为-1的idx,idx+中位m],例子里的是[3,4],也就是cabcbac中的第二个c,结果返回c
在这里插入图片描述
在这里插入图片描述
主流程就没问题了,下面看下关键的check方法,他的目的是为了返回idx,没有重复,则返回-1,如果重复,则返回重复串的起点
在这里插入图片描述
在看之前还要再进入pow方法,拿到aL1和aL2
在这里插入图片描述
contribute小于0就加模
在这里插入图片描述

最后遍历到1之后算出ans,返回
在这里插入图片描述
在这里插入图片描述
返回check接着往下走,算出h1和h2
在这里插入图片描述
遍历到中间数,累加出哈希数
在这里插入图片描述
声明一个set,存放哈希数
在这里插入图片描述
再次遍历,根据公式计算
在这里插入图片描述
模式串匹配上了就返回start,否则返回-1
在这里插入图片描述
最后整个流程都打通了,公式还要具体看,还是比较复杂的
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值