设定主串的长度为n,字串的的长度为m。
传统的暴力字符串匹配算法理论上最多需要花费O(nm)的时间复杂度才能完成串的匹配操作,但是在实际使用中,往往也能够以接近O(m+n)的时间复杂的完成匹配操作,因此现在仍被广泛使用。
而KMP算法则理论上最坏可以在O(m+n)的时间复杂度内完成字符串的匹配操作。
暴力匹配算法低效的根源是因为会进行很多次重复的匹配操作,即主串中存在回溯。而KMP算法中,主串的指针的移动方向一直是往右的。例如:
当模式串为0000001
,而主串为000000000000000000000000000000000000000001
时,由于模式中前6个字符均为“0”,主串中前45个字符均为“0”, 每趟匹配都是比较到模式的最后-一个字符时才发现不等,指针i需回溯40次,总比较次数为40x7= 280
次。
要了解子串的结构,首先要弄清楚几个概念:前缀、后缀和部分匹配值。前缀指除最后一个字符以外,字符串的所有头部子串;后缀指除第一个字符外,字符串的所有尾部子串;部分匹配值则为字符串的前缀和后缀的最长相等前后缀长度。下面以’ababa’为例进行说明:
回到最初的问题,主串为a b c a c b a b,子串为 a b c a c。
利用上述方法可以写出字串abcac
的部分匹配之为 00010
, 将其写成数组形式,就得到了部分匹配值(Partial Match,PM)的表。
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | a | c |
PM | 0 | 0 | 0 | 1 | 0 |
主串 | a | b | a | b | c | a | b | c | a | c | b | a | b |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
b字串 | a | b | c |
第一趟匹配过程:
发现c与a不匹配,前面的 2 个字符’ab’是匹配的,查表可知,最后一个匹配字符 b对应的部分匹配值为0,因此按照下面的公式算出子串需要向后移动的位数:移动位数 = 已匹配的字符数 - 对应的部分匹配值
因为2-0=2,所以将子串向后移动 2 位,如下进行第二趟匹配:
主串 | a | b | a | b | c | a | b | c | a | c | b | a | b |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
字串 | a | b | c | a | c |
第二趟匹配过程:
发现c与b不匹配,前面4个字符’abca’是匹配的,最后一个匹配字符a 对应的部分匹配值为1,4-1=3,将子串向后移动3位,如下进行第三趟匹配:
主串 | a | b | a | b | c | a | b | c | a | c | b | a | b |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
字串 | a | b | c | a | c |
子串全部比较完成,匹配成功。整个匹配过程中,主串始终没有回退,故KMP算法可以在O(n + m)的时间数量级上完成串的模式匹配操作,大大提高了匹配效率。
某趟发生失配时,如果对应的部分匹配值为 0,那么表示已匹配相等序列中没有相等的前后缀,此时移动的位数最大,直接将子串首字符后移到主串 i 位置进行下一趟比较;如果已匹配相等序列中存在最大相等前后缀(可理解为首尾重合),那么将子串向右滑动到和该相等前后缀对齐(这部分字符下一趟显然不需要比较),然后从主串i位置进行下一趟比较。
仔细观察不难发现:
Move=(j-1)-next[j]
,相当于将子串的比较指针 j 回退到j=j-Move=j-((j-1)-next[j])=next[j]+1
有时为了使公式更加简洁、计算简单,将 next 数组整体+1。因此,上述子串的next 数组也可以写成
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | a | c |
next | 0 | 1 | 1 | 1 | 2 |
最终得到子串指针变化公式j=next[j
]。
next[j]的含义是:在子串的第 j 个字符与主串发生失配时,则跳到子串的next[j]
位置重新与主串当前位置进行比较。
当i=4、j=4时,s4跟pa(b+a)失配,如果用之前的 next 数组还需要进行 s4与 P3、S4与 P2、S4与 P1这3 次比较。事实上,因为 pnext [4]-3=p4=a、pnext 13]=2=p3=a、pnext [2]-1=p2=a显然后面3次用一个和p4相同的字符跟 s4比较毫无意义,必然失配。
那么问题出在哪里呢?
问题在于不应该出现 Pj = Pnext[j]
理由是:当 Pj != Sj时,下次匹配必然是 Pnext[j] 跟 Sj 比较,如果Pj=Pnext[j],那么相当于拿一个和 Pj 相等的字符跟 Sj 比较,这必然导致继续失配,这样的比较毫无意义。那么如果出现了 Pj = Pnext[j] 应该如何处理呢?
如果出现了,则需要再次递归,将 next[j]
修正为 next[next[j]]
,直至两者不相等为止
完整代码
#include
#include
#include
#define MAXLEN 255 // 定义最大串长
// 串的结构
typedef struct
{
char ch[MAXLEN]; // 串元素
int length; // 串当前长度
} SString;
/**
* 初始化一个空串,数组下标从1开始
* SString t 要初始化的空串
*/
SString initSString(SString t)
{
// 将串长设置为0
t.length = 0;
// 将首元素赋值(没有这个也行,因为第一个元素没什么用)
t.ch[0] = '_';
return t;
}
// 串赋值操作,将char数组中的值赋值给SString结构体
/**
* 串赋值操作,将char数组中的值赋值给SString结构体
* SString t 要赋值的空串
* char chars[] 赋值的内容保存在该数组中
*/
SString strAssign(SString t, char chars[])
{
// 从下标1开始
int i = 1;
// 循环遍历将chars中的字符复制到t的ch数组中,同时长度+1
// 串结束的标志为'\0'
for(i; chars[i-1] != '\0' && i < MAXLEN; i++)
{
t.ch[i] = chars[i-1];
t.length++;
}
return t;
}
/**
* 获取Next数组的函数,用于给next数组赋值,下标也是从1开始
* SString t 模式串,即要匹配的字串
*/
void getNextVal(SString t, int nextval[])
{
int i = 1, j = 0;
// 第一个字符的next数组值默认为0
nextval[1] = 0;
// 循环赋值
while(i < t.length)
{
// 如果是第一个字符,或者当前两个字符相等,i和j同时后移一位
if (j == 0 || t.ch[i] == t.ch[j])
{
i++;
j++;
// next[i] 的值就是j的值,因为j是当前新的前缀的尾,所以j表示当前前后缀的长度
nextval[i] = j;
}
// 否则j回退,回退的值是next[j]
else
{
j = nextval[j];
}
}
}
/**
* 优化后的获取Next数组的函数,用于给next数组赋值,下标也是从1开始
* SString t 模式串,即要匹配的字串
*/
void getNext(SString t, int nextval[])
{
int i = 1, j = 0;
nextval[1] = 0;
while(i < t.length)
{
if (j == 0 || t.ch[i] == t.ch[j])
{
i++;
j++;
if (t.ch[i]!=t.ch[j])
nextval[i] = j;
else
nextval[i] = nextval[j];
}
else
{
j = nextval[j];
}
}
}
/**
* 匹配函数,用于匹配串s中知否包含串t,若包含,返回串t首字母第一次在串s中出现的位置
* SString s 父串,即要被匹配的串
* SString t 模式串,即要匹配的子串
*/
int indexKMP(SString s, SString t, int next[])
{
// 初始化下标从1开始
int i = 1, j = 1;
// 当两个串都没有匹配完时循环遍历
while (i <= s.length && j <= t.length)
{
// 如果模式串的指针为0或者当前两个串指针所指的值相同,两个指针同时后移一位
if (j == 0 || s.ch[i] == t.ch[j])
{
i++;
j++;
}
// 否则将模式串的指针前移next[j]位,也就相当于模式串向后移动next[j]位
else
{
j = next[j];
}
}
// 当两个串中有一个结束遍历,判断模式串是否遍历完,如果模式串遍历完,则匹配成功
if (j > t.length)
{
// 模式串的第一次出现下标 = i - t.length
return i - t.length;
}
else
{
// 否则s中不包含串t,返回0(因为下标是从1开始的,所以可以返回0,如果从0开始,可以返回-1)
return 0;
}
}
int main()
{
// 定义两个串
SString t, s;
// 初始化两个串
t = initSString(t);
s = initSString(s);
// 两个串的内容
char charsS[] = {'a', 'b', 'a', 'b', 'c', 'a', 'b', 'c', 'a', 'c', 'b', 'a', 'b', '\0'};
char charsT[] = {'a', 'b', 'c', 'a', 'c', '\0'};
// 分别赋值
t = strAssign(t, charsT);
s = strAssign(s, charsS);
// 创建next数组,长度位模式串长度+1,因为下标从1开始
int next[strlen(charsT)];
// 根据模式串填充next数组
getNext(t, next);
// 匹配模式串
int res = indexKMP(s, t, next);
if (res == 0)
{
printf("Not match!" );
}
else
{
printf("The index is %d", res);
}
return 0;
}