Carl代码随想录算法训练营-Day 9-28.实现strStr()、459.重复的字符串

引言

LeetCode:28.实现str Str()
KMP算法的学习始于LeetCode第28题:《实现strStr()》。
这道题给了我们一个字符串 s 和一个字符串 t ,要求在 s 字符串中找出 t 字符串的第一个匹配项的下标(下标从 0 开始)。如果 s 不是 t 的一部分,则返回 -1
我们可以很轻易地想到一个暴力搜索的方法,也就是从s的第i位开始将字符的每一位与t的每一个字符逐一比较,如果将t遍历完整了,就匹配成功,否则从s的第i+1位重复此过程。Java代码实现如下:

public int strStr1(String s, String t){
            int i=0,j=0;
            while(i<s.length()){
                if(s.charAt(i)==t.charAt(j)){
                    if(j==t.length()-1){
                        return i-j;//匹配完成
                    }
                    i++;
                    j++;
                }else{
                    i=i-j+1;//回退到搜索开始的后一位
                    j=0;
                }
            }
            return -1;
        }

这就是我们最初的串模式匹配算法,很简单很直接的思维方式,但同时也很低效很愚蠢。蠢就蠢在就算是已经遍历匹配成功的子串也会重复匹配多次。造成了其时间复杂度为O(mn)
互联网、计算机中的字符串成千上万,其长度也成千上万,这种复杂度的算法是不合适的。

初识KMP算法

三个人一起发明了KMP算法,因此有了KMP(Knuth-Morris-Pratt)算法。
要学习KMP算法,要从认识最长相等前后缀开始。

最长相等前后缀

但是在此之前,先要知道KMP算法中的前缀、后缀都是什么。

前缀

一个字符串中,符合以下条件的所有子串,叫做前缀:

  • 不含最后一个字符
  • 以字符串的第一个字符开头
  • 连续

举个例子:字符串aabaaf的前缀有
a
aa
aab
aaba
aabaa
aabaaf

后缀

一个字符串中,符合以下条件的所有子串,叫做后缀:

  • 不含第一个字符
  • 以字符串的最后一个字符结尾
  • 连续

举个例子:字符串aabaaf的后缀有
f
af
aaf
baaf
abaaf
aabaaf

最长相等前后缀

明白了前缀和后缀的概念,我们就能理解最长相等前后缀了,就是最长的相等的前缀和后缀的长度。以下两个例子展示了最长相等前后缀:
比如字符串aabaa,长度为2的前缀和后缀均为aa
比如字符串ababa,长度为3的前缀和后缀均为aba

Next数组

在暴力搜索算法中,每次匹配失败时,i需要回退到原来的位置+1j需要回退为0,这在KMP算法中得到了改变。
KMP算法定义了一个next数组,每次匹配失败时,将查询next数组的对应位置,并将j进行回退。

那么next数组表示什么?

next[i]等于模式串t的,0i的子串的最长相等前后缀的长度。

可能有点绕,但来手算一次就搞清楚了。

手算next数组

next数组有很多种计算方式,但本质都相同,本文采取其中的一种。
现在给定一个模式串tt"aabaaf",求其next数组。
next[0]=0: 0到0的子串长度为1,没有前缀后缀
next[1]=1: 0到1的子串为"aa",最长相等前后缀为"a"
next[2]=0: 0到2的子串为"aab",最长相等前后缀为""
next[3]=1: 0到3的子串为"aaba",最长相等前后缀为"a"
next[4]=2: 0到4的子串为"aabaa",最长相等前后缀为"aa"
next[5]=0: 0到5的子串为"aabaaf",最长相等前后缀为""

因此aabaafnext数组为[0, 1, 0, 1, 2, 0]

使用next数组

在求得next数组之后,我们必须要关心的是,next数组怎么用?
现在给定文本串s = "aabaabaaf",模式串t = "aabaaf"
显然next数组为[0, 1, 0, 1, 2, 0]
分别用i遍历sj遍历t。很显然,s[5]='b't[5]='f',匹配失败。
此时,将j赋值为next[j-1]=next[4]=2继续匹配。
通过这个方法,模式匹配的时间复杂度可降低为O(m+n)

代码实现KMP算法

光说不练假把式,必须要通过代码书写才能明白KMP算法为什么难为什么烦。

使用next数组进行匹配搜索

  1. 通过t生成next数组
  2. 定义两个指针:i遍历字符串sj遍历字符串t
  3. 比较s[i]s[j],如果不等,则令jnext[j-1],重复此过程直到相等或j=0
  4. 如果s[i]=t[j],如果t匹配完成,那么返回i-j,否则j1
  5. i1,如果s遍历完了,则返回-1,否则跳回至步骤3
public int strStr(String haystack, String needle) {
            int[] next = getNext(needle);
            System.out.println(Arrays.toString(next));
            for (int i = 0, j = 0; i < haystack.length(); i++) {
                while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
                    j = next[j - 1];//连续回退,直到i、j字符相同或回退到头
                }
                if (haystack.charAt(i) == needle.charAt(j)) {
                    if (j == needle.length() - 1) {//模式串匹配完整
                        return i - j;
                    }
                    j++;//当前字符匹配成功,继续匹配下一个字符
                }
            }
            return -1;
        }

计算next数组

计算next数组的步骤过程,其实与以上模式匹配的过程很相似。

private int[] getNext(String s) {
            int[] next = new int[s.length()];
            //j:前缀末尾位置、i之前包含i的子串的最长相等前后缀长度   i:后缀末尾位置
            for (int i = 1, j = 0; i < next.length; i++) {
                while (j > 0 && s.charAt(j) != s.charAt(i)) {
                    j = next[j - 1];//注意要连续回退
                }
                if (s.charAt(j) == s.charAt(i)) {
                    next[i] = ++j;//更新next数组
                }
            }
            return next;
        }

459、重复的字符串

思路分析

如果一个字符串s全都是由多个重复子串构成的话,那么我们将两个字符串s拼在一起,中间也应该至少有一个字符串s
在这里插入图片描述
因此,只需要将字符串进行拼接,将新字符串的首字符、尾字符删除,再将原字符串作为模式串,新字符串作为文本串进行匹配即可。

代码实现

实际代码实现中,为了节省Java字符串操作时间,可以创建一个新的字符数组ss,并对原字符串进行拷贝。

public boolean repeatedSubstringPattern(String s) {
            char[] t = s.toCharArray();
            char[] ss = new char[(t.length << 1) - 2];
            System.arraycopy(t, 1, ss, 0, t.length - 1);
            System.arraycopy(t, 0, ss, t.length - 1, t.length - 1);
            int[] next = getNext(t);

            for (int i = 0, j = 0; i < ss.length; i++) {
                while (j > 0 && ss[i] != t[j]) {
                    j = next[j - 1];
                }
                if (ss[i] == t[j]) {
                    if (j == t.length - 1) {
                        return true;
                    }
                    j++;
                }
            }
            return false;
        }

总结

KMP算法是一个思维复杂,代码简单的算法。这种算法,我们作为算法学习者只要将其背下来即可。

  • 21
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值