代码随想录字符串部分KMP学习总结

代码随想录链接

字符串->实现strStr()
字符串->重复的子字符串

leetcode28

力扣链接:https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/

KMP的目的:只对主串遍历一次,充分利用遍历过程中获得的信息。

一般来说,如果利用暴力匹配的方法,那么主串中的某些元素就要被多次遍历,浪费了之前遍历过程中获取的信息。假如模式串很长,并且常常是在模式串最后的几个元素匹配失败时,那么浪费的信息就更多了。极端地,如果模式串为aaaaaaaaab,而主串是aaaaa…aaaaaaa,那么暴力匹配需要遍历的元素为 (n-m)*m+m 个;而KMP只需要遍历 O(m+n) 个,其中m表示next数组的计算过程,n表示主串的遍历过程。

KMP具体怎么利用之前遍历过程中获取到的信息呢?利用前缀表,或者说next数组。前缀表告诉程序,当匹配失败时,模式串指针应该怎么移动来避免主串指针回头重新遍历,保证它可以一直往前走。

下面讨论两种极端情况。

情况一(匹配失败时前一个字符的前缀表数据显示为0):

图1
在这里插入图片描述
图2在这里插入图片描述图3
在这里插入图片描述图4
在这里插入图片描述
图5

读图提示:上面的串是主串,下面的串是模式串,模式串在字符下面的数字是前缀表或者说是next数组。蓝色填充块表示主串指针的位置,橙色填充块表示模式串指针的位置。

上面图2中,匹配到模式串的第4个字母时出现错误,根据模式串前一个字母在前缀表中的值,模式串指针应该指向模式串第1个位置(下标为0)。尽管需要对模式串从头开始匹配,看起来好像并没有节省什么时间,但是实际上主串已经认识到它不用再回头从b或者c开始对模式串的第一个元素a进行比对,因为它知道在主串指针之前已经不会再出现一个a。所以说next数组记录了遍历信息。

情况二(匹配失败时前一个字符的前缀表数据显示为一个较大的数):

在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

读图提示:上面的串是主串,下面的串是模式串,模式串在字符下面的数字是前缀表或者说是next数组。蓝色填充块表示主串指针的位置,橙色填充块表示模式串指针的位置。

上面图1中,当匹配失败时前一个字符的前缀表数据显示为一个较大的数时,模式串指针会以较小的步长跳回前面。这时模式串继续与主串后续的元素进行对比,被匹配成功的概率比较大,也是充分利用了前面遍历过程中获得的信息。

KMP主串遍历过程

根据上面的图,可以得出主串遍历的代码:

int strStr(string mainString, string patternString) {
    vector<int>next(patternString.size(), 0);
    getNext(patternString, next);  //下面会介绍这个函数怎么写

    int patternIndex = 0;
    for (int mainIndex = 0; mainIndex < mainString.size(); mainIndex++) {
        if (mainString[mainIndex] == patternString[patternIndex]) {
            if (mainIndex == mainString.size() - 1) {
                return mainIndex - patternIndex;
            }
            patternIndex++;
        }
        else if (patternIndex) {
            patternIndex = next[patternIndex - 1];
            mainIndex--;
        }
    }
    return -1;
}

next数组计算

这里的next数组就是指匹配不成功时模式串指针指向的位置,是随想录里的第二种模式。

如果把next数组的计算过程当成模式串与其本身匹配的过程,next数组的计算过程其实跟主串的遍历过程差不多,见下面的例子:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

读图提示:上下两个串是同一个串,都是模式串。但是在模式串匹配其本身的过程中,上面的串相当于新主串,下面的串相当于新模式串。在字符下面的数字是前缀表或者说是next数组。蓝色填充块表示新主串指针的位置,橙色填充块表示新模式串指针的位置。

可以看到next数组的计算过程(模式串自我匹配过程)其实跟主串的遍历过程差不多,所以两者的代码是非常相似的。只是计算next数组时,需要在每一次遍历之后给当前元素对应的next数组赋值。

用slow指针和fast指针分别标记这个过程中的新模式串和新主串。

void getNext(string& patternString, vector<int>& next) {
    int slow = 0;
    next[0] = 0;
    for (int fast = 1; fast < patternString.size(); fast++) {
        if (patternString[fast] == patternString[slow]) {
            slow++;
            next[fast] = slow;  //给next数组赋值
        }
        else if (slow) {
            slow = next[slow - 1];
            fast--;           //不用给next数组赋值,因为fast需要呆在原地直到slow==0或者
                              //patternString[fast] == patternString[slow]
        }
        else {
            next[fast] = slow;  //给next数组赋值
        }
    }
}

leetcode459

力扣链接:https://leetcode.cn/problems/repeated-substring-pattern/

KMP用于判断字符串是否由子串多次(次数大于1)构成

实际上是利用next数组判断字符串是否由子串多次(次数大于1)重复构成。

首先,如果字符串是由子串重复构成,那么[0,size-next[size-1])之间的字符构成的字符串一定就是子串。根据下图就能很容易理解。(注意:下面这个next跟我的next差1)

在这里插入图片描述
(注:图片来自代码随想录

其次,如果size%(size-next[size-1])= =0&&next[size-1],那么说明原字符串一定是由子串重复构成,返回true。这就有点难以理解了,举一两个例子确实能发现例子是具有这样的特征,但是如果证明返回false的情况就没有这样的现象呢?举一些抽象化的数据你就能理解了。需要注意的是,如果中间有一个字符毁掉了前面字符串的重复性,那么next数组会从0开始重新计算。

  • 如果s=“abcdefghijklmn”,这样的数据完全没有重复,那么next[size-1]=0;或者如果s=“abcabcabcabd”,那么也有next[size-1]=0。最后都返回false。
  • 如果s=“abcdefghijklmnabcdefghi”,这样的数据有部分重复,那么next[size-1]!=0,但是size-next[size-1]>size/2,那么size就不可能把size-next[size-1]整除,最后会返回false。
  • 如果是类似s="abc abc abd abc abc abc abc"这样的输入,那么第5个c对应的next数组数值会变成0,那么就是第一种情况了,最后会返回false。
  • 还有其他的可能性……你尽管想,不对算我输。

参考上图,你会发现这个公式是很有道理的,所以放心用吧。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值