引言
- 建议这部分内容不会的同学,可以先弄明白基本概念,这样才能知道书上的定义和定理说的是什么;然后再去理解定义和定理,理解它实现的具体原理,以及为什么要这么做,这么做的利弊;最后再仔细琢磨琢磨实现算法的代码。
- 受限于本人能力和讲解形式,有些地方可能说得有欠妥或遗漏之处,故在文末推荐了几个大神制作的教学视频,有不懂的或感兴趣的同学可以看一看。
- 码字不易,如果这篇文章对您有帮助的话,希望您能点赞、评论、收藏,投币、转发、关注。您的鼓励就是我前进的动力!
一、基本概念
- 前缀:是指除了最后一个字符外,一个串的全部头部组合。 例如在子串「ABCABX」中,A,AB,ABC,ABCA,ABCAB 均是该串的前缀。
- 后缀:是指除了第一个字符外,一个字符串全部尾部组合(不包括最后一个字符)。 如上例(ABCABX)中,B,AB,CAB,BCAB 均为该串的后缀。
- 部分匹配值:就是前缀和后缀中最长的公共前后缀的长度值。 如上例(ABCABX)中,对于元素X来说,最长前后缀共有的元素为AB两个元素,所以部分匹配值为 2。
- next数组值:等于部分匹配值加一(除第一个位置恒等于0外)。 如上例(ABCABX)中,对于元素X来说,部分匹配值为 2,所以 next 数组值为 2+1=3。
二、基本原理
- 在暴力匹配算法中,对于在子串中与首字符相等的字符,可以省略一部分不必要的判断步骤(主串的回溯)。而KMP模式匹配算法,就是为了让没必要的回溯不发生(即主串不回溯,子串少回溯)。
- KMP算法如何实现主串不回溯,子串少回溯的? KMP算法主要就是在发生主串与子串有元素不匹配时,不移动指针(指针相对于主串静止)只移动子串。并且不是将子串拨回第一个位置,而是拨到恰当位置——将最大公共前后缀里的前缀直接移动到后缀所在位置,由于前后缀是相同的,所以这样移动后,子串与主串在比较指针前面的部分,上下是匹配的。从而实现主串不回溯,子串少回溯。
三、next数组值的推导
- 公式:
n e x t [ j ] { j = 1 时,为 0 j > 1 时,为部分匹配值 + 1 \boxed { next[j] \begin{cases} j=1时,为0\\ j>1时,为部分匹配值+1\\ \end{cases} } next[j]{j=1时,为0j>1时,为部分匹配值+1 - 例题:求模式串「abcdabd」的next数组值。
解析:直接套用公式即可。如下表:
j j j | 1234567 |
---|---|
模式串 | abcdabd |
n e x t [ j ] next[j] next[j] | 0111123 |
四、nextval数组
- 改进过的KMP算法,它是在计算出next数组值的同时,如果 a 位字符与它的 next 值指向的 b 位字符相等,则该 a 位的nextval就指向 b 位的 nextval 值,如果不等,则该 a 位的nextval值就是他自己 a 位的next 值。即:
n e x t v a l [ j ] { j = 1 时,为 0 j > 1 时 { j 位字符 = = n e x t [ j ] 位字符,为 n e x t [ j ] 位字符的 n e x t v a l 值 j 位字符 ! = n e x t [ j ] 位字符,为 j 位字符的 n e x t 值 \boxed { nextval[j] \begin{cases} j=1时,为0\\ j>1时 \begin{cases} j位字符==next[j]位字符,为next[j] 位字符的nextval值\\ j位字符 !=next[j]位字符,为j 位字符的next值\\ \end{cases}\\ \end{cases} } nextval[j]⎩ ⎨ ⎧j=1时,为0j>1时{j位字符==next[j]位字符,为next[j]位字符的nextval值j位字符!=next[j]位字符,为j位字符的next值
五、实现代码
- 求next数组值函数
void GetNext(String T,int next[])
{
int i=1, k=0;
next[1]=0;
while (i<T[0]) // T[0]为串长
{
if (k==0||T[i]==T[k])
{
++i;
++k;
next[i]=k;
}
else
k=next[k];
}
}
- k是什么? k实际上就是最大公共前后缀,即部分匹配值。代码中语句「++k; next[i]=k; 」就相当于「next数组值=部分匹配值+1」。
- 该算法的主要思想就是通过 迭代 的方式,以旧值推导新值。体现如下:
1)通过指针旧值自增「 i++; 」推出指针的新值,从而实现指针的后移。
2)求某一元素的部分匹配值,即k的值。- 代码解释(写代码思路):
1)先定义指针i(注意指针i是从1开始的),部分匹配值k,及next数组。
2)在循环外,先给第一个位置处的next数组值赋值为0。
3)接着计算后续next数组值。
i. 如果部分匹配值为0或(公共)前后缀的最后一个元素值相等(通过循环累加,k值就相当于公共前后缀的长度值),此时的k值就是当前指针所指元素的部分匹配值。自增,赋值给next数组即可。
ii. 否则(即k值不为0且前后缀的最后一个元素值不相等),说明当前的k值不是对应元素的部分匹配值。将k值回溯到它的next数组值(即k=next[k]; )比较此时前后缀最后一个元素是否相等(T[i]是否等于T[k]; )。若还不等,就再将k值回溯,直至相等或k=0。
4)如此循环,直至算完该模式串的next数组值,再返回给主调函数。
- KMP算法匹配查找函数
int Index_KMP(String S, String T, int pos)
{
int i=pos; //主串指针
int j=1; //子串指针
int next[MAXSIZE];
GetNext(T, next); //此处换成GetNextval(T,nextval); 就是改进版的KMP匹配算法
while ( i<=S[0] && j<=T[0] )
{
if ( j==0 || S[i]==T[j] )
{
++i;
++j;
}
else
j=next[j];
}
if ( j > T[0] )
return i-T[0];
else
return 0;
}
- 代码解释(写代码思路):
1)先定义主串指针 i,子串指针 j 和next数组,注意主串指针 i 初始化为pos( i 可以不从主串的第一个元素开始匹配),子串指针 j 初始化为1。
2)调用GetNext函数得到next数组。
3)在指针 i,j 都小于串长时循环。在主串元素等于子串元素时(j=0的情况另讨论),说明元素匹配。将 i,j 均后移,继续比较下一对。否则,说明元素不匹配,主串指针不动,子串指针退回到恰当位置(next值所指的位置),再进行比较。
4)匹配完后:
i. 若 j 大于子串串长,说明子串与主串中的元素匹配(此时 j 等于子串串长加一),用主串指针位置减去子串串长,即得子串在主串中的首地址,返回其值。
ii. 否则,说明未在主串中找到与子串匹配的部分,返回0。- 关于if语句中的判断条件中的 j==0 :出现j=0的情况是:子串的第一个元素(next值为0)与主串比较时发生了不匹配,此时j = next[1] = 0; ,而「 ++j; 」就使得指针能再次指向子串第一个元素(不然 j=0 时,指针 j 未指向子串)。此时,由于主串元素与 j=1 的子串元素已经比过了,所以「 ++i; 」让子串的第一个元素与主串的下一个元素比较。
- 改进版nextval数组函数
void GetNextval(String T,int nextval[])
{
int i=1, k=0;
nextval[1]=0;
while (i<T[0])
{
if (k==0||T[i]==T[k])
{
++i; ++k;
//比求next数组多的代码
if (T[i]!=T[k])
nextval[i] = k;
else
nextval[i] = nextval[k];
}
else
k=nextval[k];
}
}
- 求nextval数组的代码就是在求next数组的if-else语句中再嵌套一个if-else语句即可求得nextval数组值。
- 注意:如果 a 位字符与它的 next 值指向的 b 位字符相等,则该 a 位的nextval就指向 b 位的 nextval 值,不是next值!
六、补充
- KMP算法的时间复杂度为O(n+m)。该算法仅当模式串与主串之间存在许多部分匹配的情况下,才体现出它的优势,否则两者(与暴力匹配算法相比)差异并不明显。
七、推荐资料
参考资料:
[1] 程杰. 大话数据结构. 北京:清华大学出版社, 2020.
[2]严蔚敏,吴伟民. 数据结构 (C语言版). 北京:清华大学出版社, 1997.
[3]青岛大学-王卓老师数据结构与算法课
[4]天勤考研KMP算法易懂版视频
写在文末:今天是1024程序员节,祝广大程序员及准程序员节日快乐!