子串的定位操作通常称为串的模式匹配,是各种串处理系统中最重要的操作之一。
最基本的算法就是暴力匹配法。
即从主串的第一个字符开始和子串第一个字符挨个比较。若中途匹配失败,则从主串的第二个字符开始和子串的第一个字符挨个比较。若匹配失败,则从主串的第三个字符开始和子串的第一个字符挨个比较。若匹配失败,……
下面的程序是从主串S的第pos个字符开始和子串T匹配。返回匹配成功后,子串在主串中的位置,若不存在则返回0。
int Index1(SString S, SString T, int pos)
{
int i, j = 1;
if (pos >= 1 && pos <= S[0]) {
i = pos;
while (i <= S[0] && j <= T[0]) {
if (S[i] == T[j]) {
++i;
++j;
}
else {
i = i - j + 2;
j = 1;
}
}
if (j == T[0] + 1)
return i - T[0];
else
return 0;
}
}
此算法的效率非常低,最坏的情况下,时间复杂度为O(n*m)。
比如子串为‘000000001’,主串为‘000000000000000000000000000000000000001’。
KMP算法的时间复杂度为O(n+m)。
例:(经典匹配步骤)
本质是:每次发现字符不匹配后,主串的 i 不需要回溯(第二次比较根本没必要,应该直接进行第三次比较),而是利用已经得到的部分匹配结果将模式串(即子串)向右滑动尽可能远的一段距离后再进行比较。
在上例中,i 代表主串的下标,j代表子串的下标。
第一次匹配失败时,i = 3,j = 3。
按照KMP算法,此时 i 不回溯(即 i 还是等于3),模式串应该向右滑动多少距离,即主串的第3个元素应该和子串的某一元素开始比较。与哪个元素开始比较?取决于模式串向右滑动的距离(j 的值要变为多少?)。
在上例中可以看出,当第一次匹配失败时,模式串向右滑动,j 的值应该变为 1。然后再次比较。
引出next[]数组
当匹配失败时,j 要变为的值只与模式串和当前的 j 值相关,与主串和 i 无关。(要变为的值是一个与 i 相关的函数)。所以,先根据子串本身,求出当 j=1、j=2、j=3……时的函数值。然后放入next[]数组中,当匹配失败时,需要滑动,j 值需要改变,则直接令 j = next[ j ]。
假设已经根据子串求出next[]数组。
则采用KMP算法的模式匹配程序为
int Index_KMP(SString S, SString T, int pos, int next[])
{
if (pos < 1 || pos > S[0])
return ERROR;
int i = pos;
int j = 1;
while (i <= S[0] && j <= T[0]) {
if (j == 0 || S[i] == T[j]) {
++i;
++j;
}
else
// i = i - j + 2; //i 不再回溯
// j = 1; //仅此处两行和经典算法不同
j = next[j]; //j
}
if (j > T[0])
return i - T[0];
else
return 0;
}
上面的程序仅当匹配失败时的处理和经典算法不同。
根据手工求法的步骤和例子,用代码实现求next[ ]数组。
原创代码:
void get_next(SString T, int next[])
{
next[1] = 0;
next[2] = 1;
int i = 3, j = 0, k = 0;
while (i <= T[0]) {
j = next[i - 1];
if (T[j] == T[i - 1]) // 将前一位的**字符** 与前一位的next值作为下标对应的**字符**进行比较
next[i] = next[i - 1] + 1; //相等
else { //不等,继续向前找
k = j;
j = next[j];
if (j == 0)
next[i] = 1;
while (j >= 1) {
if (T[j] == T[i - 1]) {
next[i] = next[k] + 1;
break;
}
k = j;
j = next[j];
}
if(j == 0)
next[i] = 1;
}
++i;
}
以下是上面的代码优化后(书上的):
void get_next(SString T, int next[])
{
int i = 1, j = 0;
next[1] = 0;
while (i < T[0]){
if (j == 0 || T[i] == T[j]) {
++i;
++j;
next[i] = j;
}
else
j = next[j];
}
}
Nextval 数组是基于next数组求出来的。next数组再某些情况下回有不必要的匹配,nextval是对next数组的优化。
具体步骤:若前两位相同,则nextval为0,若不同则为1,
第二位的b与第一位的a不同,所以为1
第三位的时候next值为1,第三位的a与当前next值所对应的值(即是第一位的a)相同, 所以为0。
第四位的next值为2,2对应的值为b,与当前的值不相同,nextval值为当前的next值,第四位为2
第五位的next值为2,2对应的值为b,相同,继续比,第二位b对应的next值为1,1对应的next值 为a,不同,结束,所以nextval值为1。
第六位的next值为3,3对应的值为a,不同,则nextval为next的值为3.
第七位next值为1,1对应的值为a,相同,已经是第一位,不再继续往前了。nextval则为0.
第八位的next值为2,2对应的值为b,不相同则为next的值,为2.
规律:
不同则为next值。
相同则继续往前比较,直到不同或第一位。最终则为0,不同则为对应next值。
程序如下:
void get_nextval(SString T, int nextval[])
{
int i = 1, j = 0;
nextval[1] = 0;
while (i < T[0]) {
if (j == 0 || T[i] == T[j]) {
++i;
++j;
if (T[i] != T[j])
nextval[i] = j;
else
nextval[i] = nextval[j];
}
else
j = nextval[j];
}
}