一、算法简介
KMP算法是由D.E.Knuth、J.HMorris和V.R.Pratt(其中Knuth和Pratt共同研究,Morris独立研究)发表的一个模式匹配算法,称为克努特——莫里斯——普拉特算法,简称KMP算法。该算法主要解决字符串匹配时,重复遍历的问题。和传统依次遍历的算法相比,KMP算法更适合于主串与字串之间存在许多部分匹配的情况,否则无法体现出该算法的优势。
二、算法原理
1、算法实现过程简介
算法中用到一个主字符串和一个子字符串,分别简称为 “主串” 和 “字串”,具体的功能是在主串中找到和子串匹配(相等)的连续的字符串,然后根据不同的需求进行相应的操作。
普通的字符串匹配时,选定好主串和子串中的起始位置下标,然后依次判断是否主串和子串的字符是否相等,如果出现不相等的字符,则确认本次匹配失败,结束本次匹配;如果一直判断直到最后两个字符相等,则本次匹配成功。如果匹配的结果为失败,则需要重新开始下一次的匹配,此时主串的下标需要退回到上一次的起始下标之后,子串的位置下标退回到子串的起始位置,重复上述匹配过程,直到成功匹配或者遍历完整个主串。
KMP进行字符串匹配时,首先选定主串和子串的起始位置下标,下标同时依次进行遍历比较,如果一直相等直到子串结束,则本次匹配成功;如果本次匹配失败,则主串的位置下标不用退回,保持在匹配失败的位置,子串的位置下标退回到 next 数组中存储的位置处,开始下一次的匹配,直到整个主串遍历完成或者成功匹配。
在普通字符串匹配的过程中,每开始一次匹配,主串和子串的下标都需要退回至一定的位置。KMP进行字符串匹配时,每开始一次匹配,主串的当前位置下标不用退回,子串的当前位置下标也不用退回至起始位置,而是退回至 next 数组中的指定位置。
2、next 数组
2.1、简介
nest 数组的长度和子串的长度一致,存放的是子串的当前位置下标需要退回的位置。给定一个字符串 “ababaaaba”,它对应的 next 数组中的值是{0 1 1 2 3 4 2 2 3},假定 next 数组的下标从 1 开始,则 next[7]==2,表示如果出现了主串和子串元素不匹配的情况,子串从 2 的位置重新开始比较即可,不用从 1 的位置开始。
2.2、公式
{ 0 , 当 j = 1 时 M a x { k ∣ 1 < k < j , 且 ′ P 1 . . . P k − 1 ′ = ′ P j − k + 1... P j − 1 ′ } 1 , 其 它 情 况 \left\{ \begin{matrix} 0,当 j = 1时 \\ Max\left\{k|1<k<j,且'P_1...P_k-1'='P_j-k+1...P_j-1' \right\} \\ 1,其它情况 \\ \end{matrix} \right. ⎩⎨⎧0,当j=1时Max{k∣1<k<j,且′P1...Pk−1′=′Pj−k+1...Pj−1′}1,其它情况
2.3、求值过程
在字符串 “abcabx” 中,next[6] 对应的子串为 “abcab”,其中前缀 “ab” 等于后缀 “ab”,共计两个字符相等,以此类推。根据经验,前后缀一个字符相等,对应的 next 为 2;前后缀 n 个字符相等,对应的 next 值为 n+1。
2.4、求值原理
在字符串子串 “abcabx” 中,next[6] 的值为3,假设主串为 “abcabedfd”。匹配到主串中的 “e” 和子串中的 “x” 不相等,本次匹配失败。因为本次匹配已经确定了主串中下标为 4、5 的元素和子串中下标为 4、5 的元素相等,下一次匹配时,主串的位置下标从 6 开始,主串元素下标为 6 的元素前两个元素为 “ab”,而子串的前两个元素也为 “ab”,所以子串从下标为 3 的元素起开始遍历。
在 next 数组的求值过程中,具体的值仅仅取决于子串的内容,和主串无任何关系。
2.5、案例分析
具体的求值过程可以根据公式去求取,也可以用经验去求取。
2.5.1、abcabx 公式
设子串的位置下标为 j,next[j] = k
(1)、j = 1 时,next[1] = 0;
(2)、j = 2 时,j 由 1 到 j-1 只有字符 “a”,属于其它情况,next[2] = 1;
(3)、j = 3 时,j 由 1 到 j-1 的串为 “ab”,显然 “a” 不等于 “b”,属于其它情况,next[3] = 1;
(4)、j = 4 时,j 由 1 到 j-1 的串为 “abc”,前缀字符串与后缀字符串没有相等的可能性,因此 next[4] = 1;
(5)、j = 5 时,j 由 1 到 j-1 的串为 “abca”,前缀字符串 “a”与后缀字符串 “a” 相等,则 k = 2(j-k+1=4,j=5),所以 next[5] = 2;
(6)、j = 6 时,j 由 1 到 j-1 的串为 “abcab”,前缀字符串 “ab” 与后缀字符串 “ab” 相等,则 k = 3(j-k+1=4,j = 6),所以 next[6] = 3;
所以子串 “abcabx” 的 next 数组为 {0 1 1 1 2 3}。
2.5.2、ababaaaba 经验
设子串的位置下标为 j,next[j] = k
(1)、j = 1 时,next[1] = 0;
(2)、j = 2 时,j 由 1 到 j-1 只有字符 “a”,属于其它情况,next[2] = 1;
(3)、j = 3 时,j 由 1 到 j-1 只有字符 “ab”,前缀字符串与后缀字符串不相等(0个相等)相等,因此next[3] = 1;
(4)、j = 4 时,j 由 1 到 j-1 只有字符 “aba”,前缀字符串 “a” 与后缀字符串 “a”相等,相等的字符个数为 1,因此next[4] = 2;
(5)、j = 5 时,j 由 1 到 j-1 只有字符 “abab”,前缀字符串 “ab” 与后缀字符串 “ab”相等,相等的字符个数为 2,因此next[5] = 3;
(6)、j = 6 时,j 由 1 到 j-1 只有字符 “ababa”,前缀字符串 “aba” 与后缀字符串 “aba”相等,相等的字符个数为 3,因此next[6] = 4;
(7)、j = 7 时,j 由 1 到 j-1 只有字符 “ababaa”,前缀字符串 “a” 与后缀字符串 “a”相等,相等的字符个数为 1,因此next[7] = 2;
(8)、j = 8 时,j 由 1 到 j-1 只有字符 “ababaaa”,前缀字符串 “a” 与后缀字符串 “a”相等,相等的字符个数为 1,因此next[8] = 2;
(9)、j = 9 时,j 由 1 到 j-1 只有字符 “ababaaab”,前缀字符串 “ab” 与后缀字符串 “ab”相等,相等的字符个数为 2,因此next[9] = 3;
所以子串 “ababaaaba” 的 next 数组为 {0 1 1 2 3 4 2 2 3}。
三、程序实现
//标准的 KMP 算法
//参考《大话数据结构》
#include <stdio.h>
void GetNext(char string[], int next[], int length);
int IndexKMP(char s[], int s_length, char t[], int t_length, int pos);
int main(){
char t_string[10] = " ababaaaba";
char s_string[13] = " cdababaaaba2";
printf("\n%d",IndexKMP(s_string,12+1,t_string,9+1,1));
return 0;
}
//通过计算返回字串 string 的 next 数组
//《大话数据结构》上的源码,有改动
//string 数组的第一个数据元素在无意义。在源程序中存储了 string 子串的长度
//next 数组的实际长度为 length+1。在程序中,next数据从下标 1 开始存储
//length:子串的长度 + 1
void GetNext(char string[], int next[], int length){
int i = 1;
int j = 0;
next[1] = 0;
while(i < length){
//string[i]:后缀单个字符
//string[j]:前缀单个字符
if(j == 0 || string[i] == string[j]){
++i;
++j;
next[i] = j;
}else{
j = next[j];
}
}
}
//返回子串 t 在主串 s 中第 pos 个字符之后的位置,若不存在,则返回 0
//根据《大话数据结构》的源码稍有改动
//s[]:主串;且第一个数组元素无效
//s_length:主串的长度 + 1
//t[]:子串;且第一个数组元素无效
//t_length:子串的长度 + 1
//pos:主串的起始位置
int IndexKMP(char s[], int s_length, char t[], int t_length, int pos){
int i = pos; //主串中当前下标值
int j = 1; //子串中当前下标值
int next[255] = {}; //定义 next 数组
GetNext(t, next, t_length); //获取子串的 next 数组
while(i < s_length && j < t_length){// i 小于 s 的长度且 j 小于 t 的长度时
if(j == 0 || s[i] ==t[j]){
++i;
++j;
}else{
j = next[j];
}
}
if(j > t_length - 1){ //子串的实际长度
return i - t_length + 1;
}else{
return 0;
}
}
下一篇对 KMP 模式匹配算法存在的缺陷进行改进。