KMP算法
克努特-莫里斯-普拉特操作(KMP算法),由朴素模式匹配算法改进。
适用范围,复杂度分析等,教材写的就很清楚,我之后不再赘述。
不同资料虽然核心思想相同,但具体实现不同,比如一些变量定义等,就看本文就好……
朴素模式匹配算法
说明
主串长度为n,模式串长度为m
朴素模式匹配算法:
将主串中所有长度为m的子串依次与模式串对比,直到找到一个完全匹配的子串或所有的子串都不匹配为止。
其它:
主串中最多有n-m+1个子串
注意,字符串
char str[] = "The String";
以
'\0'
结尾。
没有变量直接记录其长度,当然也不需要。
记主串为major,模式串为target,即
const char* major = "abaabaabcabaabc";
const char* target = "ababaa";
声明三个指针,作用与初始如下
char* it = major;//“正在”匹配的子串
char* it1 = major;//主串遍历用
char* it2 = target;//模式串遍历用
某次对比过程
例子
例 | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
主串S: | a | b | a | a | b | a | a | b | c | a | b | a | a | b | c |
模式串T: | a | b | a | a | b | c | |||||||||
1 | it | ||||||||||||||
a | b | a | a | b | a | a | b | c | a | b | a | a | b | c | |
a | b | a | a | b | c | X | |||||||||
2 | it | ||||||||||||||
a | b | a | a | b | a | a | b | c | a | b | a | a | b | c | |
a | X | ||||||||||||||
3 | it | ||||||||||||||
a | b | a | a | b | a | a | b | c | a | b | a | a | b | c | |
a | b | X | |||||||||||||
4 | it | ||||||||||||||
a | b | a | a | b | a | a | b | c | a | b | a | a | b | c | |
a | b | a | a | b | c |
顺便说一下,it的移动,代表模式串的“滑动”
代码
针对字符串的代码,注意字符串’\0’代表结束。
/**
* 字符串朴素模式匹配.
*
* \param major 主串
* \param target 模式串
* \return 第一次出现位置的地址
* \return NULL没找到
*/
char* mathingNative(const char* major, const char* target);
char* mathingNative(const char* major, const char* target)
{
char* it = major;//“正在”匹配的子串
char* it1 = major;//主串遍历用
char* it2 = target;//模式串遍历用
while ((*it1 != '\0') && (*it2 != '\0'))
{
if (*it1 == *it2)
{
it1++;
it2++;
}
else//某次匹配失败,则看下一个子串
{
it++;//下一个(模式串滑动到下一个)
it1 = it;//回溯至“下一个”的开头
it2 = target;//回溯至模式串开头
}
}
//*it2 == '\0'说明都匹配成功了
if (*it2 == '\0')
return it;
else
return NULL;
}
其实就是实现了标准库<string.h>中的strstr函数的功能。
返回的是地址,如果想知道是第几个(从0开始数数,从1开始数就再+1),只需要
(int)(mathingNative(s, t) - s)
KMP算法说明
由朴素模式匹配算法改进。
仍然
记主串为major,模式串为target
声明三个指针,作用与初始如下
char* it = major;//“正在”匹配的子串
char* it1 = major;//主串遍历用
char* it2 = target;//模式串遍历用
例
const char* target = "ababaa";
在朴素模式匹配算法中,一旦发现当前这个子串(it)中某个字符(*
it1和*
it2)不匹配,就只能转而匹配下一个子串(从头开始)。
也就是,it++,it1回溯到it,it2回溯到子串开头target
注意到:当出现部分匹配时,不匹配的字符之前,一定是和模式串一致的。模式串可以“滑动”一下,跳过主串的部分子串,并从另一位置继续比较。
例如 | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
主串major | a | b | a | a | b | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? |
模式串target | a | b | a | a | b | c | X | ||||||||
a | b | a | a | b | c |
具体,模式串中,当某个元素匹配失败时滑动多少(it变成啥),之后从哪里继续比较(it2跳转到哪里),由模式串本身内容决定,和主串没有关系(请往后看)。
模式串中,某个元素匹配失败时,it2跳到哪里,用next[]数组记录
根据模式串T,求出 next 数组,然后利用next数组进行匹配(主串指针it1不回溯),
模式串的“滑动”与it2的“跳转”
next[]数组、it2的“跳转”、it的“滑动”说明
注意下文中next[]数组中保存的是编号
另next[ j ] = k,则 next [ j ]表明当模式串中 j 号字符与主串中相应字符“失配”时,在模式串中需重新和主串中该字符进行比较的字符的位置。
也就是,it2要“跳转”到的位置的编号。
而 it 滑动的长度为, j - k ,即 it += j - k.
请看
根本没有出现部分匹配
某次对比中发现0号(开头元素)不匹配,也就是在之前的匹配中根本没有出现部分匹配
0 | |||||||||
---|---|---|---|---|---|---|---|---|---|
---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
当前: | it/it1 | ||||||||
master | … | ? | ? | ? | ? | ? | ? | … | |
a | b | a | b | a | a | ||||
it2 | |||||||||
之后: | it/it1 | ||||||||
master | … | ? | ? | ? | ? | ? | ? | … | |
a | b | a | b | a | a | ||||
it2 |
要直接看下一个“子串”。记next[0] = 0;吧
之后需要
it++;
it1 = it;
出现部分匹配
在之前的匹配过程中,出现部分匹配,前面是匹配的
某次对比中发现1号(开头后一个元素)不匹配
1 | |||||||||
---|---|---|---|---|---|---|---|---|---|
---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
当前: | it | it1 | |||||||
master | … | a | ? | ? | ? | ? | ? | … | |
a | b | a | b | a | a | ||||
it2 | |||||||||
之后: | it/it1 | ||||||||
master | … | a | ? | ? | ? | ? | ? | … | |
a | b | a | b | a | a | ||||
it2 |
it2指向了0号,故
next[1] = 0;
显然任何串的,next[1]都一样为0。
模式串向后滑动 1 - 0 = 1
某次对比中发现2号不匹配
2 | |||||||||
---|---|---|---|---|---|---|---|---|---|
---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
当前: | it | it1 | |||||||
master | … | a | b | ? | ? | ? | ? | … | |
a | b | a | b | a | a | ||||
it2 | |||||||||
之后: | it/it1 | ||||||||
master | … | a | b | ? | ? | ? | ? | … | |
a | b | a | b | a | a | ||||
it2 |
it2指向了0号,故
next[2] = 0;
模式串向后滑动 2 - 0 = 2
某次对比中发现3号不匹配
3 | |||||||||
---|---|---|---|---|---|---|---|---|---|
---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
当前: | it | it1 | |||||||
master | … | a | b | a | ? | ? | ? | … | |
a | b | a | b | a | a | ||||
it2 | |||||||||
滑动后: | it | it1 | |||||||
master | … | a | b | a | ? | ? | ? | … | |
a | b | a | b | a | a | ||||
it2 |
it2指向了1号,故
next[3] = 1;
模式串向后滑动 3 - 1 = 2
某次对比中发现4号不匹配
4 | |||||||||
---|---|---|---|---|---|---|---|---|---|
---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
当前: | it | it1 | |||||||
master | … | a | b | a | b | ? | ? | … | |
a | b | a | b | a | a | ||||
it2 | |||||||||
滑动后: | it | it1 | |||||||
master | … | a | b | a | b | ? | ? | … | |
a | b | a | b | a | a | ||||
it2 |
it2指向了2号,故
next[4] = 2;
模式串向后滑动 4 - 2 = 2
某次对比中发现5号不匹配
5 | |||||||||
---|---|---|---|---|---|---|---|---|---|
---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
当前: | it | it1 | |||||||
master | … | a | b | a | b | a | ? | … | |
a | b | a | b | a | a | ||||
it2 | |||||||||
滑动后: | it | it1 | |||||||
master | … | a | b | a | b | a | ? | … | |
a | b | a | b | a | a | ||||
it2 |
it2指向了3号,故
next[5] = 3;
模式串向后滑动 5 - 3 = 2
显然,所有一般不匹配情况都只需
//得到不匹配的元素编号,用it1 - it 也行
uint16_t j = it2 - target;
it += j - next[j];//“滑动”,记录位置,用 it = it1 - next[j];也行
it2 = target + next[j];//it2跳转
利用next数组进行匹配
首先是,利用next数组进行匹配的代码
/**
* 字符串模式匹配(KMP).
*
* \param major 主串
* \param target 模式串
* \return 第一次出现位置的地址
* \return NULL没找到
*/
char* matchingKMP(const char* major, const char* target);
char* matchingKMP(const char* major, const char* target)
{
int* next = getNext(target);//得到next数组
char* it = major;//“正在”匹配的子串
char* it1 = major;//主串遍历用
char* it2 = target;//字串遍历用
while (*it1 != '\0' && *it2 != '\0')
{
if (*it1 == *it2)
{
it1++;
it2++;
}
else
{
if (it2 == target)//之前根本没有匹配的
{//看下一子串
it++;
it1 = it;
}
else//之前有匹配的
{
it2 = target + next[(it2 - target)];//“跳转”,“滑动”
it = it1;//更新位置
}
}
}
free(next);
if (*it2 == '\0')
{
return it;
}
else
return NULL;
}
求next[]数组
说明
观察前面的例子
---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | |
---|---|---|---|---|---|---|---|---|
3 | a | b | a | ? | ? | ? | … | |
a | b | a | b | a | a | |||
4 | a | b | a | b | ? | ? | … | |
a | b | a | b | a | a | |||
5 | a | b | a | b | a | ? | … | |
a | b | a | b | a | a |
it2要跳到的位置,编号next[j]为(j>=2)
对前j个字符(0到j-1,不含j号字符)形成的新字符数组,寻找最大正整数k,且k<j,使得“前k个和后k都一样”
其中前k个称“前缀”,后k个称“后缀”
例如:
3中,0号’a’和2号’a’一样,则next[3]=1
4中,0-1和2-3,“ab”,"ab"一样,则next[4]=2
5中,0-2和2-4,“aba”,"aba"一样,则next[5]=3
---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | |
---|---|---|---|---|---|---|---|---|
3 | a | b | a | |||||
4 | a | b | a | b | ||||
5 | a | b | a | b | a |
如果找不到,则next[j]为0
例如:
2中,找不发到,则next[2]为0
---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | |
---|---|---|---|---|---|---|---|---|
2 | a | b |
或者说:
next[j]=
前j个字符(0到j-1,不含j号字符)形成的新字符数组的最大前后缀长度
对于next[1],it2跳到0号位置,所以next[1]=0。
其实也符合上面的要求,就是找不到的情况
1 | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
---|---|---|---|---|---|---|---|---|
a | ? | ? | ? | ? | ? | … | ||
a | b | a | b | a | a | |||
a | b | a | b | a | a |
特殊的next[0],it2跳到0号位置,所以next[0]=0,(按我的代码其实不用管它,是几都行)
0 | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
---|---|---|---|---|---|---|---|---|
? | ? | ? | ? | ? | ? | … | ||
a | b | a | b | a | a | |||
a | b | a | b | a | a |
暴力求解
由上面的说明,可以直接得到容易理解的代码,其效率当然不怎么样。
/**
* 求next数组便于理解的方法.
*
* \param str 要求的数组
* \return 求得的数组首地址
* \return NULL 空间申请失败或参数错误
*/
uint16_t* getNextEasyUnderstand(const char* str);
uint16_t* getNextEasyUnderstand(const char* str)
{
//得到字符串长度
uint16_t length = strlen(str);
//不应当传入空字符串
if (length < 1) return NULL;
uint16_t* next = (uint16_t*)malloc(length * sizeof(uint16_t));
if (next == NULL) return NULL;
for (uint16_t i = 0; i < length; i++) next[i] = 0;//开始都置为0
for (uint16_t j = 1; j < length; j++)
{
//对前j个字符(0到j-1,不含j号字符)形成的新字符数组
//看看前k个和后k个是不是都一样
//k从大到小,得到的就是最大值
for (uint16_t k = j - 1; k > 0; k--)
{
if (strncmp(str, str + j - k, k) == 0)//它==0代表相等
{//找到了
next[j] = k;
break;
}
}
}
return next;
}
next[]正常求解方法
需要使用“递推”的思想,并且看成字符串匹配问题,模式串既是“主串”也是“模式串”
设
const char* target = "ABACABABAC";//长度10
可以暴力求解得到其next[]数组为
uint16_t next[10] = {0, 0, 0, 1, 0, 1, 2, 3, 2, 3};
以此为例进行说明
设,已知next[j] = k, j>=1
一、k>=1
说明,前k个和后k个完全一致,即
′
t
0
.
.
.
t
k
−
1
′
=
′
t
j
−
k
.
.
.
t
j
−
1
′
't_0...t_{k-1}' = 't_{j-k}...t_{j-1}'
′t0...tk−1′=′tj−k...tj−1′
看第k+1个(t[k])和第j+1个(t[j])
1.若 t[j] == t[k]
′ t 0 . . . t k − 1 t k ′ = ′ t j − k . . . t j − 1 t j ′ 't_0...t_{k-1}t_{k}' = 't_{j-k}...t_{j-1}t_{j}' ′t0...tk−1tk′=′tj−k...tj−1tj′
说明,前k+1个和后k+1个完全一致。 则
next[j+1] = k + 1;
例如,在target中,已知next[6]=2,求next[7]时
-------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
it | it1(t[j]) | j=6 | |||||||||||
A | B | A | C | A | B | A | B | A | C | ||||
A | B | A | C | A | B | A | B | A | C | ||||
it2(t[k]) | k=2 |
发现t[6]==t[2],故next[7] = 2 + 1 = 3。
循环中这部分对应代码为
uint16_t j = it1 - target;
uint16_t k = it2 - target;//注意这里!
//在循环中,k必须这样得到,而不能 k = next[j];,原因看后面
next[j + 1] = k + 1;
it1++;
it2++;
2.若 t[j] != t[k]
就是,部分匹配,而k号元素不匹配了,这就是前面将KMP中的情况。
需要滑动,跳转,然后在循环中继续进行判断(也就是可能需要滑动不止一次)。最终会转为其它情况。
“滑动、跳转”后得到新的k, k’= next[k],k’‘=next[k’]…… k 不再是开始的next[j],所以在循环中必须
uint16_t k = it2 - target;//注意这里!
//在循环中,k必须这样得到,而不能 k = next[j];
例如,在target中,已知next[7]=3,求next[8]时
-------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
it | it1(t[j]) | j=7 | |||||||||||||
A | B | A | C | A | B | A | B | A | C | ||||||
A | B | A | C | A | B | A | B | A | C | ||||||
it2(t[k]) | k=3 | ||||||||||||||
it | it1 | ||||||||||||||
A | B | A | C | A | B | A | B | A | C | ||||||
A | B | A | C | A | B | A | B | A | C | ||||||
it2 |
然后,在循环中继续进行判断。
这部分的代码
uint16_t k = it2 - target;//注意这里!
//在循环中,k必须这样得到,而不能 k = next[j];
it2 = target + next[k];//it2跳转,“滑动”
it = it1 - next[k];//记录位置
二、k==0
之前不存在已经匹配的
1.若 t[j] == t[k]
之前没有匹配的,现在有了一个匹配的,故
next[j+1] = 1;
也可以在循环中使用,一、中1.的代码,(k=0)
next[j+1] = k + 1;
例如,在target中,已知next[4]=0,求next[5]时
-------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
it/it1(t[j]) | j=4 | ||||||||||||||
A | B | A | C | A | B | A | B | A | C | ||||||
A | B | A | C | A | B | A | B | A | C | ||||||
it2(t[k]) | k=0 |
得next[5]=1
2.若 t[j] != t[k]
之前没有匹配的,现在任然没有,故
next[j+1] = 0;
然后看后面的
it++;
it1 = it;
例如(换个例子啊)
abcdabcd中next[3]=0
-------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
it/it1(t[j]) | j=3 | ||||||||||||||
a | b | c | d | a | b | c | d | ||||||||
a | b | c | d | a | b | c | d | ||||||||
it2(t[k]) | k=0 |
而t[3]!=t[0],故next[4]=0。
循环结束条件
注意循环的结束条件,next[j]由0号至j-1号字符决定,故当it1指向最后一个字符时结束循环。
例如next[8]=2
-------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
it | it1 | j=8 | |||||||||||||
A | B | A | C | A | B | A | B | A | C | ||||||
A | B | A | C | A | B | A | B | A | C | ||||||
it2 |
求得next[9]=3,这时求完了整个next数组,it1++,到了最后一个字符,此时应当结束循环
求next[]数组代码
由上面的说明,可以得到代码
/*
描述:求next数组
注意:next数组长度等于要求的字符串的数组长度
不应当传入空字符串
*/
uint16_t* getNext(const char* target)
{
//得到字符串长度
uint16_t length = strlen(target);
//不应当传入空字符串
if (length < 1) return NULL;
uint16_t* next = (uint16_t*)malloc(length * sizeof(uint16_t));
if (next == NULL) return NULL;
next[0] = 0;
if (length < 2) return next;
next[1] = 0;
const char* theEnd = target + length - 1;//最后一个非'\0'字符
char* it = target+1;//正在比较的子串
char* it1 = it;//主串遍历用
char* it2 = target;//字串遍历用
//递推,j=1的已知知道j=2,故初始it为target + 1,it2为target
while (it1 != theEnd )//到了最后一个字符,此时应当结束循环
{
uint16_t j = it1 - target;
uint16_t k = it2 - target;
if (*it1==*it2)
{
next[j + 1] = k + 1;
it1++;
it2++;
}
else
{
if (k==0)//根本没有出现部分匹配,特殊不匹配情况
{
next[j + 1] = 0;
it++;//看下一子串
it1++;//看下一子串
}
else//有部分匹配,(一般不匹配情况)
{
it2 = target + next[k];//it2跳转,“滑动”
it = it1 - next[k];//记录位置
}
}
}
return next;
}
优化
前面定义的next[]数组在某些情况下仍有不足。
例如
const char* target = "aaaab";//模式串
uint16_t next[] = {0, 0, 1, 2, 3};
进行KMP算法匹配,当2号字符不匹配时
-------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
---|---|---|---|---|---|---|---|---|---|
it | it1 | ||||||||
master | … | a | a | ? | ? | ? | ? | ? | … |
a | a | a | a | b | |||||
it2 | |||||||||
滑动 | |||||||||
it | it1 | ||||||||
master | … | a | a | ? | ? | ? | ? | ? | … |
a | a | a | a | b | |||||
it2 | |||||||||
滑动 | |||||||||
it/it1 | |||||||||
master | … | a | a | ? | ? | ? | ? | ? | … |
a | a | a | a | b | |||||
it2 |
需要滑动两次
注意到,当2号字符’a’不匹配时,it1所指位置必不为’a’。
而next[2]=1,1号位置字符仍然为’a’,此次比较必然失败。完全可以跳过,it2转而直接到next[1]=0的位置。
4号字符不匹配时也类似,完全可以跳过几次滑动过程。
也就是说我们可以把next[]数组优化成nextval[]数组,
优化方法是,如果 j 号字符,与 k = next[j] 号字符相同,则next[j]优化成next[k]。
示例中
const char* target = "aaaab";//模式串
uint16_t next[] = {0, 0, 1, 2, 3};
可将next[]优化成
uint16_t nextval[] = {0, 0, 0, 0, 3}
代码
/**
* 对next[]数组进行优化.
*
* \param target 模式串
* \param next next[]数组
*/
void nextOptimize(const char* target, uint16_t* next);
void nextOptimize(const char* target, uint16_t* next)
{
uint16_t number = strlen(target);
if (number < 3)//至少有三个元素才有“优化”一说
return;
for (uint16_t j = 0; j < number; j++)
{
uint16_t k = next[j];
if (target[j] == target[k])
{
next[j] = next[k];
}
}
}
显然,我们可以直接求优化后的数组,只需在给next[j+1]赋值后加个判断就好。
//省略一些代码
//...
next[j + 1] = k + 1;
if (target[j + 1] == target[k + 1])//优化
next[j + 1] = next[k + 1];
//...
//省略一些代码
完整代码
/**
* 求nextval数组.
*
* \param target 模式串
* \return 求得的next数组地址
* \return NULL 传入了空字符串或内存申请失败
*/
uint16_t* getNextval(const char* target);
uint16_t* getNextval(const char* target)
{
//得到字符串长度
uint16_t length = strlen(target);
//不应当传入空字符串
if (length < 1) return NULL;
uint16_t* next = (uint16_t*)malloc(length * sizeof(uint16_t));
if (next == NULL) return NULL;
next[0] = 0;
if (length < 2) return next;
next[1] = 0;
const char* theEnd = target + length - 1;//最后一个非'\0'字符
char* it = target + 1;//正在比较的子串
char* it1 = it;//主串遍历用
char* it2 = target;//字串遍历用
//递推,j=1的已知知道j=2,故初始it为target + 1,it2为target
while (it1 != theEnd)
{
uint16_t j = it1 - target;
uint16_t k = it2 - target;
if (*it1 == *it2)
{
next[j + 1] = k + 1;
if (target[j + 1] == target[k + 1])//优化
next[j + 1] = next[k + 1];
it1++;
it2++;
}
else
{
if (k == 0)//根本没有出现部分匹配,特殊不匹配情况
{
next[j + 1] = 0;
it++;//看下一子串
it1++;//看下一子串
}
else//有部分匹配,(一般不匹配情况)
{
it2 = target + next[k];//it2跳转,“滑动”
it = it1 - next[k];//记录位置
}
}
}
return next;
}