一、串的定义
#define MAXLEN 255
typedef struct {
char ch[MAXLEN];
int length;
}SString;
定义结构体SString,结构体成员分别为:
1.char型字符;
2.int型串长length。
二、串的朴素模式匹配算法(BF算法)
1.代码实现
int Index_BF(SString S, SString T)
{
int i = 1, j = 1;
while (i <= S.length && j <= T.length)
{
if (S.ch[i] == T.ch[i])
{
++i;
++j;
}
else
{
i = i - j + 2;
j = 1;
}
}
if (j > T.length)
return i - T.length;
else
return 0;
}
2.代码解释
(1)形参SString S、SString T分别为子串与模式串;
(2)定义int i指向子串的对应位置;int j指向模式串的对应位置;
(3)while循环:
①循环条件:i <= S.length && j <= T.length;即i和j的指向都没有超出主串和模式串时;
②if条件:若S.ch[i] == T.ch[i]即主串和子串的字符相等时,对i和j都自增1;
若不满足if条件时,i=i-j+2(指向主串下一个字符);j=1(回溯到第1个字符重新对比);
第一次循环不满足if条件,i指向下一个A,j继续指第1个L:
第二次循环不满足if条件,i指向下一个M,j继续指向第一个L:
第三次循环不满足if条件,i指向下一个L,j继续指向第一个L:
第四次循环满足if条件,i、j自增1:
第五次循环满足if条件,i、j自增1:
第六次循环满足if条件,i、j自增1:
下一次不满足循环条件,退出while循环,此时j=4>T.length,返回i - T.length=4。
(4)如果遍历完主串都没有匹配到模式串,退出循环时应该是i>S.length,j=1;此时j不大于T.length,返回0;
(5)存在一种特殊情况:上面例子中主串S如果为 I A M L O,当L O匹配后,i=6,j=2;此时i>S.length退出while循环,但j<T.length,返回0。
朴素模式匹配算法存在的缺点是:
1.若主串长度为n,模式串长度为m,则时间复杂度O(mn),较高;
2.朴素模式匹配算法每次不匹配时,都会将i回溯到紧挨着的下一个对比子串中,如果碰到极端例子,会经常性地对指针进行回溯,浪费大量时间。
故提出对BF算法的优化——KMP算法:
三、KMP算法
1.优化思路
主串指针不回溯,只有模式串指针回溯;
2.举例说明
假设模式串为abcabd,分以下几种情况讨论:
①若当前字符匹配的情况下,则i++;j++;
②若j=1时发生不匹配,也就是第一个字符a没有匹配,则i++;j应该回溯到1,开始重新找a进行匹配;但这时的j=1不是直接回溯到1,而是回溯到j=0然后让i和j都自增1;
③若j=2时发生不匹配,也就是第二个字符b没有匹配,则j应该回溯到1重新开始找a进行匹配;
④若j=3时发生不匹配,也就是第三个字符c没有匹配,则j应该回溯到1重新开始找a进行匹配;
⑤若j=4时发生不匹配,也就是第四个元素a没有匹配,则j应该回溯到1重新开始找第一个a进行匹配;
(至此临时解释一下为什么以上情况需要回溯到开头重新找:因为abc这三个元素没有重复的,至少后面几个元素(bc)不是第一个元素a,如果有和开头元素或者开头几个元素重复的,那就可以少回溯几个位置,从重复的元素后开始找,比如下面这些情况:)
⑥若j=5时发生不匹配,也就是第五个元素b没有匹配,这时可以不用从头开始回溯了,解释如下:
当第五个元素没有匹配,也就是说前四个元素abca已经匹配了,那abca里出现了第四个元素a和第一个元素a相同,可以直接跳过第一个a开始找,即j=2,期待下一个i是不是b就可以了。这时少回溯1个位置,节省了时间;
⑦若j=6时发生不匹配,也就是第六个元素d没有匹配,则可知前五个abcab已经匹配,发现后面的ab和开头的ab相同,则可以跳过前两个ab,即j=3,期待下一个i是不是c,这时少回溯2个位置,节省时间;
⑧若全部匹配则匹配成功。
3.代码实现:
int Index_KMP1(SString S, SString T, int next[])
{
int i = 1, j = 1;
while (i <= S.length && j <= T.length)
{
if (j = 0 || S.ch[i] == T.ch[i]) //j=0的条件适用于第一个元素不匹配
{
++i;
++j;
}
else
j = next[j]; //调用next数组对应元素
}
if (j > T.length)
return i - T.length;
else
return 0;
}
4.补充解释
(1)⑥⑦两种情况中所提到的跳过a、跳过ab直接往后找,是因为我们找到了a、ab和前面重复。给出以下两个术语:
①前缀:包含第一个字符,且不包含最后一个字符的子串;
②后缀:包含最后一个字符,且不包含第一个字符的子串;
也就是我们在找重复元素时,实际上是找了某一个子串中相同的前缀和后缀,那这个子串是谁呢?
⑥⑦两种情况中,我们分别在abca和abcab中搜索了相同的前后缀,所以应该是匹配失败前的所有字符所组成的串。
(2)j=1不匹配,回溯j=0;(特殊情况,实际上是回溯到j=1)
j=2不匹配,回溯j=1;
j=3不匹配,回溯j=1;
j=4不匹配,回溯j=1;
j=5不匹配,回溯j=2;
j=6不匹配,回溯j=3;
(3)这种回溯方式我们可以定义一个int next[]数组来存放这些回溯信息,当j=k且发现字符不匹配时,令j=next[k],基于上面的分析,给出以下求next数组的方法:当第j个字符匹配失败时,由前1~j - 1个字符组成的串记为S,则next[j] = S的最长相等的前后缀长度 + 1;
(4)KMP算法时间复杂度O(m+n)。
四、next、nextval数组
1.next数组
(1)至此我们已经知道KMP算法的核心关键就在于求出next数组,给出手算和机算两种方法:
①手算:当第j个字符匹配失败时,把前面1~j-1个字符提取出来,找到相同的前后缀,记其长度为k(若没有相同的前后缀则长度k=0),则next[j]=k+1;
②机算:
void get_next(SString T, int next[])
{
int i = 1, j = 0;
next[1] = 0;
while (i < T.length)
{
if (j == 0 || T.ch[i] == T.ch[j])
{
++i;
++j;
next[i] = j;
}
else
{
j = next[j];
}
}
}
(2)结合给出的求next数组函数我们可以将KMP算法改进为:
int Index_KMP2(SString S, SString T)
{
int i = 1, j = 1;
int next[T.length + 1];
get_next(T, next);
while (i <= S.length && j <= T.length)
{
if (j == 0 || S.ch[i] == T.ch[i])
{
++i;
++j;
}
else
{
j = next[j];
}
}
if (j > T.length)
return i - T.length;
else
return 0;
}
2.nextval数组
(1)nextval数组是对next数组的优化,实际上next数组存在一种特殊情况下的问题,举个例子:
(2)之前的例子中,a b c a b d 所求的next数组为 0 1 1 1 2 3;
①按原来的方法,第四个字符a不匹配时,将j回溯到j=1,开始比较主串与模式串第一个字符a是否相等,但我们已知主串当时这个字符已经和a不匹配了,所以再比较一次就没有意义了;
②故将next[4]不再赋予1而赋予0,也就是和next[1]相等,此时i++;j++;跳过i对第一个字符a的匹配进行下一个i对a的匹配;
③当第五个字符b不匹配时,原本j=2,开始比较主串和b是否相等,但已知和b不匹配,比较失去意义;现在将j=next[2]=1,回溯j=1,比较i对应的字符和模式串第一个字符a是否相等;
④优化后的next数组为 0 1 1 0 1 3。
(3)nextval数组的求法:
①先算出next数组;
②令nextval[1]=0;
③
for (int j = 2; j <= T.length; j++)
{
if (T.ch[next[j]] == T.ch[j])
nextval[j] = nextval[next[j]];
else
nextval[j] = next[j];
}
(4)用nextval数组替代next数组即可提高效率。