KMP算法简述
在上文我们提到了KMP算法的基本思路,即减少主串的遍历次数,最好只遍历一次来优化暴力算法。
我们在暴力算法中,指向主串的指针在每完成一次匹配后都要回溯到此次匹配起始位置的下一位,时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)(n是主串的长度)。
暴力匹配
通过上面的动画不难看出,暴力匹配算法在不断的回溯。而这大大增加了时间复杂度。那么我们优化的方向就显而易见了。
优化方向
我们可以通过减少回溯的过程,甚至达到主串指针不回溯来优化。那么又如何达到不回溯呢?
基本方法
其实,当我们遍历主串时,其实也可以同时拿到主串的信息,如果能利用好这些已经匹配过的信息,就能实现主串指针不回溯。下面我们通过一个一个动画来理解下这个过程。(我们称匹配的字符串为模式串)
KMP
不难发现: 在第一次匹配不上的时候,主串指针并没有回溯。并且模式串指针并没有指向到第0位,而是指向了第1位。这是因为通过刚才的匹配过程,已经知道了主串的第1位是
A
A
A,所以主串的第1位是和模式串的第0位是相匹配的,所以主串的指针无需回溯到第1位与模式串的第0位进行再次匹配。因此可以直接进行主串第2位和模式串第1位的匹配。
那么接下来的问题就是:如何得知当发生不匹配时,模式串指针应当指向模式串的哪一位置。 下面,我们引入
n
e
x
t
next
next数组用于描述这一问题。
next 数组的作用
我们建立
n
e
x
t
next
next数组用于描述当模式串的第
n
n
n位和主串的某一位不匹配时,模式串指针应该指向什么位置的问题。
也就是说,
n
e
x
t
next
next存储了模式串在第
n
n
n位不匹配时,模式串指针指向的位置。
即:若
n
e
x
t
=
[
N
E
X
T
0
,
N
E
X
T
1
,
N
E
X
T
2
,
.
.
.
,
N
E
X
T
n
]
next=[NEXT_0,NEXT_1,NEXT_2,...,NEXT_n]
next=[NEXT0,NEXT1,NEXT2,...,NEXTn]当在
i
n
d
e
x
=
0
index=0
index=0时不匹配,那么
i
n
d
e
x
=
n
e
x
t
[
0
]
=
N
E
X
T
0
index=next[0]=NEXT_0
index=next[0]=NEXT0。以此类推,当在
i
n
d
e
x
=
n
index=n
index=n时不匹配,那么
i
n
d
e
x
=
n
e
x
t
[
n
]
=
N
E
X
T
n
index=next[n]=NEXT_n
index=next[n]=NEXTn。
这样,我们就方便的解决了模式串指针的指向更改问题。
下面,我们假设我们已经得到了
n
e
x
t
next
next数组,我们根据上面的逻辑给出代码实现。
C代码实现
我们的任务是写一个函数,返回模式串在主串第一次出现的位置,如果没有出现,返回-1.
首先,我们应当定义两个指针用于指向主串( s t r str str)和模式串( p a t pat pat)。同时建立next数组。(假设我们已经拿到了)
int index_str = 0;
int index_pat = 0;
int* next = getNextArray(pat);//get next array
检查与迭代
然后,我们检查对应的字符是否匹配,如果匹配,那应该检查下一对字符,于是index_str、index_pat都指向下一个字符。
if (str[index_str] == pat[index_pat]) {
index_pat++;
index_str++;
}
如果不匹配,那么应当更新index_pat指向next对应位置,然后检查此时index_pat与index_str指向的字符是否匹配。在这个过程中,由于index_str指向的字符还应该与更新后的index_pat指向的字符比较,那么index_str不应当指向下一位。
else {
index_pat = next[index_pat];
}
但是,如果index_pat指向的是pat的第0位,并且pat的第0位和str的当前位匹配不上,那么index_str就永远不会更新,于是出现死循环。所以更改为:
else {
if (index_pat == 0)//This condition must lay before assign to index_pat
++index_str;//Because we add this condition is for that in next loop we can visit the old str element and than compare to the new pat element
index_pat = next[index_pat];//if we lay judge statement after the assignment that will appear a situation which index_pat==0 that meet the judge condition that make index_str+=1 that we can not visit this element in next loop
}
然而,我们需要注意的是,如果将index_pat的更新操作放到判断操作之前,可能会出现跳过当前主串str[index_str]与后面pat[index_pat]的比较的情况。所以顺序如上。
循环退出条件
我们需要遍历主串 直到遍历到主串末尾,即str[index_str] == ‘\0’,或者是直到已经检查到模式串的末尾(表明模式串已经和主串完全匹配上),即pat[idnex_pat]==’\0’。
函数返回值
如果没有匹配成功那么返回-1:
if (pat[index_pat] != '\0')return -1;//如果都匹配成功那么最后一定指向'0',反之,则未匹配成功
匹配成功:返回第一次出现的位置:
return index_str - index_pat;
完整代码
int kmpAlogramith(char str[], char pat[]) {
int* next = getNextArray(pat);//get next array
int index_str = 0;
int index_pat = 0;
while (str[index_str] != '\0') {
if (str[index_str] == pat[index_pat]) {
index_pat++;
index_str++;
}
else {
if (index_pat == 0)//This condition must lay before assign to index_pat
++index_str;//Because we add this condition is for that in next loop we can visit the old str element and than compare to the new pat element
index_pat = next[index_pat];//if we lay judge statement after the assignment that will appear a situation which index_pat==0 that meet the judge condition that make index_str+=1 that we can not visit this element in next loop
}
if ( pat[index_pat] == '\0')
break;
}
if (pat[index_pat] != '\0')return -1;//如果都匹配成功那么最后一定指向'0',反之,则未匹配成功
return index_str - index_pat;
}
下面我们详细介绍 n e x t next next数组的得出。
next数组的得出
对于
s
t
r
str
str、
p
a
t
pat
pat的匹配过程,如果
p
a
t
pat
pat在第
n
n
n位之前和
s
t
r
str
str对应匹配,那么我们无需知道
s
t
r
str
str是什么,因为我们只需要str和pat已经匹配的几位。并且这几位是和pat的已经匹配的部分完全相同的。所以,
n
e
x
t
next
next数组的得出与
s
t
r
str
str无关。下面我们讨论如何通过pat得出next数组。我们还是假设
n
e
x
t
=
[
N
E
X
T
0
,
N
E
X
T
1
,
N
E
X
T
2
,
.
.
.
,
N
E
X
T
n
]
next=[NEXT_0,NEXT_1,NEXT_2,...,NEXT_n]
next=[NEXT0,NEXT1,NEXT2,...,NEXTn]假设pat与next的各位如下图:
首先,我们不得不讨论
N
E
X
T
n
NEXT_n
NEXTn对于
p
a
t
pat
pat的意义:
我们在 “基本方法” 中的讨论,之所以不用比较主串的第1位和模式串的第0位,是因为我们已经知道他们时相等的(
s
t
r
[
1
]
=
=
p
a
t
[
0
]
str[1]==pat[0]
str[1]==pat[0]);然而,实际上我们时这样得出的上面的这个结论:
p
a
t
[
1
]
=
=
s
t
r
[
1
]
,
p
a
t
[
1
]
=
=
p
a
t
[
0
]
=
>
s
t
r
[
1
]
=
=
p
a
t
[
0
]
pat[1]==str[1],pat[1]==pat[0]=>str[1]==pat[0]
pat[1]==str[1],pat[1]==pat[0]=>str[1]==pat[0]也就是说,我们实际在
p
a
t
pat
pat中比较的是第0位和第1位。
推而广之: 实际上,
N
E
X
T
n
NEXT_n
NEXTn指向的
C
n
C_n
Cn的前若干位有如下关系
C
n
−
1
=
=
C
N
E
X
T
n
−
1
−
1
C_{n-1}==C_{NEXT_{n-1}-1}
Cn−1==CNEXTn−1−1
C
n
−
2
=
=
C
N
E
X
T
n
−
1
−
2
C_{n-2}==C_{NEXT_{n-1}-2}
Cn−2==CNEXTn−1−2
C
n
−
3
=
=
C
N
E
X
T
n
−
1
−
3
C_{n-3}==C_{NEXT_{n-1}-3}
Cn−3==CNEXTn−1−3
.
.
.
.
.
.
.
.......
.......
C
n
−
N
E
X
T
n
=
=
C
0
C_{n-NEXT_{n}}==C_{0}
Cn−NEXTn==C0
简单的说就是:当
C
n
C_n
Cn与
C
N
E
X
T
n
C_{NEXT_n}
CNEXTn对齐的时候,
N
E
X
T
n
NEXT_n
NEXTn表示
C
N
E
X
T
n
C_{NEXT_n}
CNEXTn前的元素和
C
n
C_n
Cn元素的连续匹配长度。如下图:
1.next 的长度
明显, n e x t . l e n = p a t . l e n next.len=pat.len next.len=pat.len,因为pat的每一位都应该对应一个如果在此位不匹配时,index_pat的跳转位置。
2. N E X T 0 、 N E X T 1 NEXT_0、NEXT_1 NEXT0、NEXT1的确定
如果pat在第0位就已经不匹配,那么index_pat的跳转位置只能是0;
如果pat在第1位不匹配,那么容易知道,index_pat的跳转位置也是0;(当然,next.len>1)
3.next 其他值的确定
下面,我们使用数学归纳法得出
n
e
x
t
next
next的其他值。
如果我们已经得出了
C
n
+
1
C_{n+1}
Cn+1的前面的
N
E
X
T
n
NEXT_{n}
NEXTn,现在,我们求
N
E
X
T
n
+
1
NEXT_{n+1}
NEXTn+1:
根据上面的推广,我们可以知道:
{
C
N
E
X
T
n
−
1
,
C
N
E
X
T
n
−
2
,
C
N
E
X
T
n
−
3
,
.
.
.
,
C
0
}
≡
{
C
n
−
1
,
C
n
−
1
,
C
n
−
3
,
.
.
.
,
C
n
−
N
E
X
T
n
}
\left\{C_{NEXT_n-1},C_{NEXT_n-2},C_{NEXT_n-3},...,C_0\right\}\equiv\left\{C_{n-1},C_{n-1},C_{n-3},...,C_{n-NEXT_n}\right\}
{CNEXTn−1,CNEXTn−2,CNEXTn−3,...,C0}≡{Cn−1,Cn−1,Cn−3,...,Cn−NEXTn}
那么,如果
C
n
=
=
C
N
E
X
T
n
C_n==C_{NEXT_n}
Cn==CNEXTn,通过上面的理解,现在匹配的长度是
N
E
X
T
n
+
1
NEXT_n+1
NEXTn+1。
所以
N
E
X
T
n
+
1
=
N
E
X
T
n
+
1
NEXT_{n+1}=NEXT_n+1
NEXTn+1=NEXTn+1如果
C
n
!
=
C
N
E
X
T
n
C_n!=C_{NEXT_n}
Cn!=CNEXTn,那么现在匹配的长度就变成了0。
那么
N
E
X
T
n
+
1
=
0
NEXT_{n+1}=0
NEXTn+1=0
由此,我们可以由
N
E
X
T
0
、
N
E
X
T
1
NEXT_0、NEXT_1
NEXT0、NEXT1确定next的所有值。
下面,给出代码实现。
next数组获取C实现
我们的任务是创建一个函数,来返回next数组。
首先,我们定义两个指针。
i
n
d
e
x
0
index0
index0用于顺序指向
p
a
t
pat
pat的元素,
i
n
d
e
x
1
index1
index1用于指向由上一个NEXT指向的位置来进行比较、迭代。
当然,我们需要先建立一个next数组。并且,如果
p
a
t
.
l
e
n
<
=
1
pat.len<=1
pat.len<=1那么直接返回next就可以。
如果
p
a
t
.
l
e
n
>
1
pat.len>1
pat.len>1,我们已经知道了
n
e
x
t
[
0
]
,
n
e
x
t
[
1
]
next[0],next[1]
next[0],next[1]所以index0从2开始即可。
int len = strlen(pat);
int* next = (int*)malloc(len * sizeof(int));
next[0] = 0;
if (len > 1)next[1] = 0;
else return next;
int index0 = 2;
int index1 = 0;
循环计算
更新 i n d e x 1 index1 index1
index1 = next[index0 - 1];
计算 n e x t [ i n d e x 0 ] next[index0] next[index0]
if (pat[index0 - 1] == pat[index1])
next[index0] = next[index0 - 1] + 1;
else next[index0] = 0;
遍历指针右移一位:
index0++;
循环退出条件:知道读取到字符串末尾。
完整代码
int* getNextArray(char pat[]) {
int len = strlen(pat);
int* next = (int*)malloc(len * sizeof(int));
next[0] = 0;
if (len > 1)next[1] = 0;
else return next;
int index0 = 2;
int index1 = 0;
while (pat[index0] != '\0') {
index1 = next[index0 - 1];
if (pat[index0 - 1] == pat[index1])
next[index0] = next[index0 - 1] + 1;
else next[index0] = 0;
index0++;
}
return next;
}
全部代码思路
int* getNextArray(char pat[]) {
int len = strlen(pat);
int* next = (int*)malloc(len * sizeof(int));
next[0] = 0;
if (len > 1)next[1] = 0;
else return next;
int index0 = 2;
int index1 = 0;
while (pat[index0] != '\0') {
index1 = next[index0 - 1];
if (pat[index0 - 1] == pat[index1])
next[index0] = next[index0 - 1] + 1;
else next[index0] = 0;
index0++;
}
return next;
}
int kmpAlogramith(char str[], char pat[]) {
int* next = getNextArray(pat);//get next array
int index_str = 0;
int index_pat = 0;
while (str[index_str] && pat[index_pat]) {
if (str[index_str] == pat[index_pat]) {
index_pat++;
index_str++;
}
else {
if (index_pat == 0)//This condition must lay before assign to index_pat
++index_str;//Because we add this condition is for that in next loop we can visit the old str element and than compare to the new pat element
index_pat = next[index_pat];//if we lay judge statement after the assignment that will appear a situation which index_pat==0 that meet the judge condition that make index_str+=1 that we can not visit this element in next loop
}
}
free(next);
if (pat[index_pat] != '\0')return -1;//如果都匹配成功那么最后一定指向'0',反之,则未匹配成功
return index_str - index_pat;
}
写在最后
以上仅是笔者的一些粗浅的理解,实际上在确定next数组的时候使用了最长真前后缀这一关键概念。但笔者对这一概念理解不够透彻,而直接使用next迭代也没有问题,所以没有在本文体现这一概念。而实际上,使用这一概念理解next数组或许更加接近其本质。
(ps :笔者是菜鸟)