阿云的数据结构学习笔记(KMP算法,新手向)
先说点题外话,因为非科班 的原因,最终还是决定考研,于是在重新学习数据结构的时候觉得有些值得记录的东西应该都会放进学习笔记里面。
写KMP是因为正好复习到这来了(手动狗头)
一、对简单匹配问题的处理
首先,看看一般情况,也是最容易想到的。
从主串的第一个位置 S1 起,和模式串的第一个字符开始进行比较,如果相等,那么继续下一位的比较,直到模式串最后一个,则匹配成功。或者出现不匹配的情况,那么就将主串此时的记录位置 Si 后移一位,再重新与模式串进行比较。
代码如下:
bool index(string mainstr,string patternstr){
int i = 0, j = 0, k = 0;
while(i < mainstr.size() && j < patternstr.size() ){
if(mainstr[i] == patternstr[j]){
++i;
++j;
}
else{
j = 1;
i = ++k;//此处 k 是用来记录主串与模式串比较时的起始位置
}
}
if(j >= pattern.size())//由于不匹配将回溯到1,所以当j 大于模式串的长度时,说明匹配成功。
return true;
else
return false;
}
以主串S = " ABABCABCAC " ,模式串 = " ABCAC " 为例:
第一趟 | A | B | A | B | C | A | B | C | A | C |
A | B | C | A | C | ||||||
第二趟 | A | B | A | B | C | A | B | C | A | C |
A | B | C | A | C | ||||||
第三趟 | A | B | A | B | C | A | B | C | A | C |
A | B | C | A | C | ||||||
第四趟 | A | B | A | B | C | A | B | C | A | C |
A | B | C | A | C | ||||||
第五趟 | A | B | A | B | C | A | B | C | A | C |
A | B | C | A | C | ||||||
第五趟 | A | B | A | B | C | A | B | C | A | C |
A | B | C | A | C |
在这里我们可以看到, 在第三趟匹配失败的时候,发现模式串 P 与主串 S 不匹配,即在 Pi 失去匹配,但从 P1 一直到 Pi - 1 都是匹配成功的。而且我们可以明显观察到已匹配串中重复出现了字符 A ,那么我们可以不从主串中往后挪一位再重新进行比较。而是直接将模式串的A移动到下一个A出现的位置(此处为 P3 ),因为在匹配串是与模式串在失去匹配位 Pi 前都是相等的(即没有必要再去比较,因为都不相等,我们直接跳过不相等的,来到相等位)。由此我们衍生出KMP的思路。
二、KMP匹配
首先,明确KMP的核心思想是不退回指针,近似的向右 “ 滑动 ” ,将相等的字符 “ 滑 ” 过去。
我们将每一次的匹配设为某状态 Sn ,假设匹配成功时的状态为 Sk ,再取中间的某一个状态 Sni。
得出下表:
主串 | * * * | Si - j + 1 | Si - j + 2 | * * * | Si - t + 1 | Si - t + 2 | * * * | Si - 2 | Si - 1 | Si | * * * * * * |
匹配情况 | = | = | * * * | = | = | * * * | = | = | ≠ | ||
Si | P1 | P2 | * * * | Pj - t +1 | Pj - t + 2 | * * * | Pj - 2 | Pj - 1 | Pj | * * * * * * | |
匹配情况 | = | = | * * * | = | = | ? | |||||
Sk | P1 | P2 | * * * | Pt - 2 | Pt - 1 | Pt | * * * |
我们可以近似的看作将模式串向右 “ 滑动 ” 。
不难发现,在由 Si 向 Sk 移动时,总满足 P1 = Pj - t + 1 , P2 = Pj - t + 2 , * * * * * * , Pj - 1 = Pt - 1。(此处 j,t 均不唯一)
同时也能发现,由于是发生在已匹配串中,所以我们可以抛开主串,完全只看模式串。
这样我们就可以根据模式串来得到一个数组,这个数组会告诉你如果当前不匹配了,应该回溯到模式串中的哪一个位置。(或者说是告诉你怎么滑,滑到哪一位)
Next数组
从上面我们知道了,通过 “ 滑动 ” 的分析方式来解决当主串与模式串出现不匹配的问题。所以需要一个next数组将 Pi 中的 i 进行回溯。
以模式串 “ ABABABB ” 为例:
模式串 | A | B | A | B | A | B | B |
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
next [ j ] | 0 | 1 | 1 | 2 | 3 | 4 | 5 |
上述表格表明的是当 Pj 与主串失去匹配时候该怎么回溯。(当 j 失去匹配时往前回溯)
例如:
当j = 3时,由于已匹配串为 “ AB ”,而当前位置 ≠ A,所以退回 1,再看主串的当前字符是否等于 P1 。
当j = 4时,已匹配串为 “ABA”,存在重复,而当前位置 ≠ B,所以会退回 next[ j ] (next[ j ] = 2)。(因为已匹配的字符串中出现重复,即 1 和 3 。所以不需要再对1进行匹配。)
后续省略***
代码示例如下:
void GetNext(string str, int next[]){
int i = 0, j = 0;
next[1] = 0;
while(i < str.size()){
if(j == 0 || str[i] == str[j]){//j = 0 时为特殊情况。
++i;
++j;
next[i] = j;//下一位的返回位置。
}
else
j = next[j];//当前不相同,回溯。
}
}