1、KMP算法
参考视频:bilibili
参考博客:KMP
时隔一个月,KMP算法又忘了。
第一步:复习前缀、后缀的概念
例1:字符串"abcdefg"
前缀集合:{“a”,“ab”,“abc”,“abcd”,“abcde”,“abcdef”},g被排除了
后缀集合:{“g”,“fg”,“efg”,“defg”,“cdefg”,“bcdefg”},a被排除了
例2:字符串"ab"
前缀集合:{“a”}
后缀集合:{“b”}
例3:字符串"a"
前缀集合:{“a”}
后缀集合:{""}
第二步:复习next[i]的值的含义
参考博文:https://blog.csdn.net/x__1998/article/details/79951598
next[i]表示字符串str[0…i-1]中前缀、后缀集的交集中最长子串长度
例1:字符串“abab”,问next[3]=1的含义??
子串str[0…2]=“aba”
前缀集合:{“a”,“ab”}
后缀集合:{“a”,“ba”}
那么交集就是:“a”,长度为1,则next[3]=1
约定:next[0]=-1,这是标志,不代表长度。
第三步:手撕Getnext()代码
思路:输入的字符串既是目标字串,又是模式字串。
void Getnext(int next[],String t)
{
/* 创建2个指针,left指前缀子串,right指向后缀子串 */
int left = -1, right = 0;
next[0]=-1;
while(right < t.length - 1) //right从0到n-2
{
/* left == -1 情况有2种:
* 初始化后首次进入循环
* 多次匹配失败回溯后,left回溯到了最初开始的地方
* 以上2种情况都意味着:没有1个字符匹配成功,那就双指针++*/
if(left== -1 || t[left] == t[right])
{
/* 匹配成功也双指针++ */
left++;right++;
if (t[left] == t[right]) {
/* 对特殊情况的优化,例:字符串“aaaaaaax” ,连续重复字符*/
/* left与right此刻是相邻着的,且2指针指向的字符相同,那回溯的地方也相同 */
next[right] = next[left];
} else {
/* 一般情况:next[]存放子串长度 */
next[right] = left; //left经过++后,既是下次循环的index,又是本次循环的最大前后缀交集字串的长度
}
}
else left = next[left]; //回溯,当模式匹配串失配时,next数组对应的元素指导应该用T串的哪个元素作为前缀进行下一轮的匹配
}
}
第四步:手撕KMP算法框架
int KMP(String s,String t)
{
//s表示目标串,t表示模式串
int next[t.length],si=0;ti=0;
Getnext(t,next);
while(si<s.length && ti<t.length)
{
if(ti == -1 || s[si] == t[ti])
{
si++;
ti++;
}
else ti=next[ti]; //失配,回溯模式串指针,避免了模式串开头几个字符的重复匹配
//利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置
}
if(ti >= t.length)
return (si-t.length); //匹配成功,返回子串的位置
else
return (-1); //没找到
}
2、Sunday算法
模式匹配算法都需要一个源串的滑动指针 i 和模式串的滑动指针 j 。普通的匹配算法在字符失配时需要同时回溯 i 和 j ,复杂度为O(m*n)。
- KMP匹配算法就是在失配时不回溯源串指针 i,只回溯 j 到最长前缀处使得复杂度达到O(n)。
- Sunday算法不仅不回溯 i,而且可以使 i 跨越模式串前进,进一步降低复杂度。
C++程序
i 作为模式串T的滑动指针,L作为匹配位置的起点,那么 L+i 就是源串S的滑动指针
string sunday(string S, string T) {
static int cnt;
int N = S.length(), M = T.length(), L = 0, i = 0;
//L表示匹配开始的位置,i表示滑动指针
map<char, int> sun;
for (int c = 0; c < T.size(); ++c) {
sun[T[c]] = c;
}
// 区间S[0, N)
while ((L + i) < N) {
// 匹配成功,指针右移
while (i < M && S[L + i] == T[i]) {
++i;
// 匹配结束
if (i == M) return T;
}
// 匹配失败
if (S[L + i] != T[i]) {
// 若字符S[L + M + 1]不曾在模式串中出现过,则移动下次匹配开始的位置
while ((L < N - M) && (sun.find(S[L + M]) == sun.end())) {
L += M - 1; //L表示匹配开始的位置
}
// 若已经到达边界,则退出循环,L表示匹配开始的位置
if ((L + M) >= N) {
break;
}
// 回溯i,重新定位匹配起始位置L
// L += (L + M) - (L + sun[S[L + M]]);
L += M - sun[S[L + M]];
i = 0;
}
}
return "";
}
记忆点:理解为什么要与S[L+M]比较?
记模式串为S,子串为T,N = S.length(),M = T.length(), 设S中此次第一个匹配的字符位置为L。那么,此次参与匹配的字符为S[L,L+M)区间。
当S[L+i]与T[i]失配时,显然,当前匹配区间S[L,L+M)的剩余长度肯定不足够了,那么字符S[L+M]肯定要参加下一轮的匹配,并且T至少要与S[L+M]匹配才有可能与整个S区间匹配。
记忆点:理解L的前进步长
Sunday算法需要对模式串T进行预处理,记录T中每个字符的最后一次出现的位置。可以使用数组或者哈希表,如
map<char, int> sun;
for (int c = 0; c < T.size(); ++c) sun[T[c]] = c;
当S[L+i]与T[i]失配时,就要考虑S[L+M]是否在T中出现过,位置是哪里,举例:
L=0,每次匹配的起始位置
i=3,模式串的滑动指针
M=4,模式串的长度
S:abcceabcaabcd
T:abcd
发现d与c不匹配。此时S[L+M]=='e',没有出现在T中。于是:
S:abcceabcaabcd
T:-----abcd
可见,L一次性增加了4+1个单位
发现d与a不匹配。此时S[L+M+1]=='a',T中最后出现在T[0],而M - 0 = 4 - 0。于是:
S:abcceabcaabcd
T:---------abcd
可见,L一次性增加了4个单位,成功匹配。