补充:先介绍下字符串前后缀。
字符串的前缀:符号串左部的任意子串(或者说是字符串的任意首部)
字符串的后缀:符号串右部的任意子串(或者说是字符串的任意尾部)
例:字符串:“abcdef”
该字符串前缀有:空串、“a”、“ab”、“abc”、“abcd”、“abcde”、“abcdef”
该字符串后缀有:空串、“f”、“ef”、“def”、“cdef”、“bcdef”、“abcdef”
一、暴力字符串匹配
在讲kmp之前,先来看一下传统的暴力字符串匹配,假设主串S的长度为M,子串T的长度为N,时间复杂度O(M * N)。如下for循环。
for (int i = 0; i < strlen(S); i++) {
for (int j = 0; j < strlen(S); j++) {
if (S[i] == S[j]) {
......
} else {
......
}
}
}
可以看到,此处没有任何优化,已处理过的字符对未处理的字符没有任何指导意义,那是否有一种算法,可以让已处理过的字符对未处理的字符有指导意义。
二、KMP算法
kmp算法就不介绍了,我们先来看一下kmp核心思想:
1.查找字符串之前,先要对查找的字符串做一个分析,这样可以加速匹配过程。
2.匹配失败S串不用回溯。
例1:
S串:”abcdabce“
T串:”abce"
开始比较:
第一轮比较 S[0] = T[0]
第二轮比较 S[1] = T[1]
第三轮比较 S[2] = T[2]
第四轮比较 S[3] 不等于 T[3]了,这时怎么办呢,老办法从S[1],T[0]开始再比一次,这样效率很低,让我看看有哪些步骤可以省去:
已知:T[0] !=T[1] !=T[2] !=T[3],S[0] = T[0],S[1] = T[1],S[2] = T[2]
证明:S[1] != T[0],S[2] != T[0],
解:因为 S[1] = T[1],S[2] = T[2]
所以 S[1] != T[0],S[2] != T[0],等价于T[1] != T[0],T[2] != T[0]
又因为 T[0] !=T[1] !=T[2] !=T[3]
所以 T[1] != T[0],T[2] != T[0]成立,证明完毕。
综上:我们可以直接从,S[3]、T[0], 开始比较,省去一些没有意义的比较过程,所以得出如果我们先分析出了T串的一些特性(如T串各个字符不相对),那是可以加速一些匹配过程的。
是不是有了一点感觉呢,再让我们再来看第二个例子,你会更加有感觉:
例2:
S串:”aabaaaaaab“
T串:”aabaac"
开始比较:
第一轮比较 S[0] = T[0]
第二轮比较 S[1] = T[1]
第三轮比较 S[2] = T[2]
第四轮比较 S[3] = T[3]
第五轮比较 S[4] = T[4]
第六轮比较 S[5] 不等于 T[5]了,那下一轮我们要如何比较呢,那些过程可以省去,不急,让我们来分析一下已经比较过的字符串"aabaa",该串的前缀串"aa”,后缀串"aa"是一样的。
已知 T[3] = T[0],T[4] = T[1],S[0] = T[0], S[1] = T[1],S[3] = T[3],S[4] = T[4]
可求得:S[3]=T[0], S[4]=T[1]
综上 S[3]=T[0], S[4]=T[1],就不用比较了,直接求S[5] 是否 等于 T[2]就可以了。省去了一部分步骤。
看到这里有感觉了吗,对T串的预处理,可以加速我们的匹配过程。
在KMP算法中,对T串的预处理之后,会生成一个数组,我们称他为next数组,来加速匹配过程。
三、next数组的推导。
一句话概述:对i位置求next数组的值时,取出0 ~ i-1位置对应的字符串,求出该字符串最长且一样的前后缀长度(不包含自身)。
默认定义:
next[0] = -1;
next[1] = 0;
例1:
“aabaac”,对应的next数组为:
数组索引 0 1 2 3 4 5
字符串: a a b a a c
next数组: -1 0 1 0 1 2
具体分析:
i = 2时,0 ~ 1对应的字符串为aa,最长且一样的前缀和后缀都是a,所以值是1.
i = 3时,0 ~ 2对应的字符串为aab,不存在一样的前缀和后缀,所以值是0.
i = 4时,0 ~ 3对应的字符串为aaba,最长且一样的前缀和后缀都是a,所以值是1.
i = 5时,0 ~ 4对应的字符串为aabaa,最长且一样的前缀和后缀都是aa,,所以值是2.
例2:
“aaaaac”,对应的next数组为:
数组索引 0 1 2 3 4 5
字符串: a a a a a c
next数组: -1 0 1 2 3 4
具体分析:
i = 2时,0 ~ 1对应的字符串为a,最长且一样的前缀和后缀都是a,所以值是1.
i = 3时,0 ~ 2对应的字符串为aa,最长且一样的前缀和后缀都是aa,所以值是2.
i = 4时,0 ~ 3对应的字符串为aaa,最长且一样的前缀和后缀都是aaa,所以值是3.
i = 5时,0 ~ 4对应的字符串为aaaa,最长且一样的前缀和后缀都是aaaa,,所以值是4.
例3:
“abcdef”,对应的next数组为:
数组索引 0 1 2 3 4 5
字符串: a a a a a c
next数组: -1 0 0 0 0 0
具体分析:
i = 2时,0 ~ 1对应的字符串为a,不存在一样的前缀和后缀,所以值是0.
i = 3时,0 ~ 2对应的字符串为aa,不存在一样的前缀和后缀,所以值是0.
i = 4时,0 ~ 3对应的字符串为aaa,不存在一样的前缀和后缀,所以值是0.
i = 5时,0 ~ 4对应的字符串为aaaa,不存在一样的前缀和后缀,所以值是0.
注:若0~i位置的最长且相等的前后缀字符串为x,则0到i + 1位置的前后缀字符串最长长度不会超过x + 1。
next数组的原理是不是清楚了,先看下相关代码。
int *get_next(char *T)
{
int len = strlen(T) + 1; // 防止越界,这里多申请一个空间
int *next = malloc(sizeof(int) * len);
memset(next, 0, sizeof(int) * len);
next[0] = -1;
next[1] = 0;
int i = 2;
int index = 0; /* 前缀字符串最后一个字符的索引 */
while (i < len - 1) {
/* 判断前后缀字符串最后一个字符是否一致,若一致,则得到i位置最大前后缀字符串长度 */
/* 若不一致,index不断回跳,直致index等于0 */
if (T[i - 1] == T[index]) {
next[i++] = ++index;
} else if (index > 0) {
index = next[index];
} else {
next[i++] = 0;
}
}
return next;
}
接下来我们在结合next数组和上面举的例子,看下kmp怎么玩的
例1:
S串:”abcdabce“
T串:”abce"
数组索引 0 1 2 3
字符串: a b c e
next数组: -1 0 0 0
当S中的‘e’ 与T中的’e’没有匹配时,S中的‘a’ 可以直接跟T中的’a’开始继续匹配
S串:”aabaaaaaab“
T串:”aabaac"
数组索引 0 1 2 3 4 5
字符串: a a b a a c
next数组: -1 0 1 0 1 2
当S中的‘a’ 与T中的’c’没有匹配时,S中的‘a’ 可以直接跟T中的’b’开始继续匹配,因为’aa’已经比过了,不用在重复匹配了,这里是不是一下就发现了next数组的作用。
总上当S[x]与T[y]不匹配时,S[x]可以继续与T[next[y]]进行匹配,这就是next数组提供了一些指导作用,也 可以说是分析了T串后,加速了匹配过程,让我看下代码。
/* 若匹配返回下标,若不匹配返回-1 */
int kmp(char *S, char *T)
{
if (S == NULL || T == NULL ) {
return -1;
}
int S_len = strlen(S);
int T_len = strlen(T);
if (S_len < T_len) {
return -1;
}
int x = 0;
int y = 0;
int *next = get_next(T);
/*
时间复杂度是O(n)
最好情况和最差情况很好推
aaaeaaafaaat
aaah
类型这种情况是2n,不会存在每次x+1,y都要重头开始匹配的情况,回退的长度不回超过 x - y的最大值 n
*/
while (x < S_len && y < T_len) {
if (S[x] == T[y]) {
x++;
y++;
} else if (next[y] == -1) { /* 此时y是0 */
x++;
} else {
y = next[y];
}
}
free(next);
/* y匹配完了,才说明S包含T */
return y == T_len ? x - y : -1;
}
这个时间复杂度理解起来比较费劲
让我们先来看while里循环
1.x++,y++ 不会超过strlen(S)
2.x++ 不会超过strlen(S)
3.y-- y累计减少次数不会超过strlen(S);代码中也有最坏情况说明。
x上升,y才会上升,y减少到0,就不会减少了,所以y累积减少次数不会超过strlen(S);
或者用x-y来表示,x上升,y减少,最大差值为strlen(S);
获取next数组的时间复杂度也类似。
若strlen(S)=M,strlen(T)=N,时间复杂度表示为O(M + N)
可以leetcode跑下对应代码。
leetcode28:https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/
四、关于next数组的优化
若明白了上面说的,这里举一个例子就知道了
举个例子:
S=“aaaaaabbbbbbbb”;
T=“aaaaaac”;
对T进行分析
索引 0 1 2 3 4 5 6
T a a a a a a c
next -1 0 1 2 3 4 5
优化后 -1 0 0 0 0 0 5
优化:
当S中的第一个b,与T最后一个a比较时,可以直接跳到S的开头,不用反复做kmp代码这步操作y = next[y]一步;
核心思想:
若T[i] = T[index],则next[i] = next[index];减少重复判断,不用判断多次’a’
若T[i] != T[index],和之前一样。
下面看下代码,跟未优化的代码相比,修改了几行。
int *get_next(char *T)
{
int len = strlen(T) + 1; // 防止越界,这里多申请一个空间
int *next = malloc(sizeof(int) * len);
memset(next, 0, sizeof(int) * len);
next[0] = -1;
next[1] = 0;
int i = 2;
int index = 0; /* 前缀字符串最后一个字符的索引 */
while (i < len - 1) {
if (T[i - 1] == T[index]) {
/* 判断前后缀字符串最后一个字符是否一致 */
//next[i++] = ++index;
++index;
next[i] = T[i] == T[index] ? next[index] : index;
i++;
} else if (index > 0) {
index = next[index];
} else {
next[i++] = 0;
}
}
return next;
}