昨天休息了一天,今天虽然是周末,但无奈我是要准备复试的考研人,所以还是得低强度地学起来!
顺便唠唠嗑,之前在b站上关注的up主“刘磊的成长日记”,小伙子大不了我几岁,现在已经上班一段时间了,看到以别人的视角去记录在外地互联网公司上班的日常让我更直接地认识了it行业打工人的生活。小伙很自律,平时爱健身、周末也会去学习,这可能就是为什么优秀的人会越来越优秀的原因吧!
今日任务(1.14):上午学两道算法题(弄懂那个滑动窗口)+下午开始跟着demo学java项目+晚上跟着视频深度学习
难绷,花了3个多小时才只搞完了一道算法题,就是这个kmp算法。
之前在王道课上学的是手算next数组,所以对代码实现是一窍不通,看了老半天才勉强懂个大概,以下是acwing上原题:
acwing题解上都是从数组下标为1开始存储的,但我觉得那样不方便理解,我选择的是从下标为0开始的数组进行存储:
#include <bits/stdc++.h>
using namespace std;
const int M = 1000010;
const int N = 100010;
int n, m;
char s[M], p[N];
int nextNum[N];
void BuildNext(){
for(int i = 0; i < n; i++)
nextNum[i] = 0;
int j = 0;
for(int i = 1; i < n; i++){
while(j > 0 && p[i] != p[j]) // 不同情况
j = nextNum[j - 1]; // 反复令j回退,直到j为0或s[i] == s[j]
if(p[i] == p[j]) // 相同情况
j++; // 匹配成功,最长相等前后缀变长
nextNum[i] = j; // 更新操作,令next[i] = j,以便计算next[i + 1]
}
}
void KmpSearch(char s[], char p[]){
for(int i = 0, j = 0; i < m; i++){
while(j > 0 && s[i] != p[j])
j = nextNum[j - 1];
if(s[i] == p[j])
j++;
if(j == n){
cout << i - j + 1 << ' '; // 匹配成功,文本串结束位置减去模式串长度即为起始位置
j = nextNum[j - 1]; // 将j回退到一定位置,让i加1继续进行比较(
}
}
}
int main(){
cin >> n >> p >> m >> s;
BuildNext();
KmpSearch(s, p);
}
关键点1:求next数组
BuildNext()方法中i从模式串数组p的下标1开始,而j指向模式串下标0,next[0]初始化为0。
随后开始比较:
1.当p[i] != p[j]的时候,不匹配,说明最长的情况不成立,需要缩短,即j = next[j - 1]
举个例子:
p[8]和p[16]不匹配的话,那么最长的一种可能1~8和9~16就不可能是p的最长前后缀匹配;于是只能继续往短的方向去考虑,再去考虑多短的呢?固定方法是j = next[j - 1](别问就是硬记),所以外面是一个循环而非仅仅一次if判断,因为可能还会不断地缩小。
接下来看p[4]和p[16]是否匹配,如此循环下去,直到j = 0,便不满足while的循环条件j > 0了。
2.如果其中出现匹配成功的话,那么最长相等前后缀就会增加,并且i++、j++
3.这样每一次循环都会成功算出一个next数组对应的i下标
关键点2:模式串与主串相匹配
1.因为我设定的主串和模式串都是从0开始存储的,所以i和j都从0开始往后走,当s[i] != p[j]时,j回退到j = next[j - 1]这个位置(同样,别问就是硬记)
2.如果主串与模式串相匹配的话(s[i] == p[j]),则i++、j++
3.最后当j滑动到最后末位的时候(j = n时)说明匹配完成,返回初始位置的起始下标i - j + 1,随后将j回退到一个位置j = next[j - 1](硬记),外层循环i + 1,随后继续寻找比较。
我不太愿意去深究回退的原理,因为我觉得就算现在记住了以后也大概率会忘,不如记住一个固定的模板以便以后能迅速写出来。