文章目录
KMP算法
K
M
P
KMP
KMP算法是字符串匹配中的一个较好的算法,它的目的是在给定一个模式串
p
p
p和原字符串
q
q
q中,找到
p
p
p在
q
q
q中第一次出现的位置。
如上图所示,可以很简单的看出字符串 p p p在字符串 q q q中第一次出现的位置,如果位置从1开始计算的话,那么第一次出现的位置就是3。那么如何使用一个简单的程序来实现该功能呢,首先最容易想到的就是暴力法,不断的使用 p p p去对应 q q q中的字符,如果遇到一个不相等的,则 p p p从第一个字符开始重新开始和 q q q后续的字符进行比较。这其中会产生一些重复比较的过程,如果去掉这些重复比较的过程,就是 K M P KMP KMP算法所作的事情,关于哪里重复,后续会继续讲到。
下面的代码为普通的暴力法,是参考我之前写过的 K M P KMP KMP的内容https://blog.csdn.net/weixin_44267007/article/details/109272225,这里面 p p p是原字符串, q q q是模式串,和我们之前的定义刚好相反,但是接下来还是按照之前的定义来进行讲解,暴力法代码实现太过简单,就不赘述了。
int getIndex(string p,string q)
{
int i=0;
int j=0;
while(i<(int)p.length() && j<(int)q.length())
{
if(p[i] == q[j])
{
i++;
j++;
}
else
{
i=i-j+1;
j=0;
}
if(j==q.length())
return i-q.length();
}
return -1;
}
前置知识
在深入了解 K M P KMP KMP算法之前,还需要对于串中的一些概念和 C + + C++ C++中字符串库 s t r i n g string string做一些介绍,因为这些工具都会大量运用在之后的原理解释中。
串的基本概念
在字符串的基本概念这一块,主要介绍字符串的前缀和后缀,以及最长相同前缀后缀。接下来放出定义。
前缀:从字符串第一个字符开始的任意长度的连续子串
后缀:从字符串最后一个开始往前的任意长度的连续子串
举个例子,对于字符串“ababc",前后缀如下所示。
定义 | 串 |
---|---|
前缀 | “”(空串),“a”,“ab”,“aba”,“abab”,“ababc” |
后缀 | “”(空串),“c”,“bc”,“abc”,“babc”,“ababc” |
通过上述例子应该对前缀和后缀有了了解,那么最长相同前缀后缀,对于一个字符串,在知道其前缀后缀的基础上,如果前缀和后缀中相同的串就叫做该串的相同前缀后缀,而在所有的相同前缀后缀中,最长的一个叫做最长相同前缀后缀。
例如对于字符串"eefegeef"而言,最长相同前缀后缀为“eef”,长度为3。也可以通过下图来理解,即两个字符串的最长相交长度。
C + + C++ C++中 s t r i n g string string库的函数
接下来对于代码实现中需要使用到的 s t r i n g string string库函数进行分析, s t r i n g string string库需要在代码最前面的 i n c l u d e include include区域添加 C + + C++ C++中的头文件 < s t r i n g > <string> <string>。接下来主要对其中的一些函数进行介绍,其实也很简单。
首先是length()和size()函数,这两个函数的功能都是返回一个字符串的长度,这里的长度不包括在 C C C语言中学习到的结束符。但是需要注意的是,这两个函数返回的值不是 i n t int int类型,而是 u n s i g n e d _ i n t unsigned\_int unsigned_int类型,也就是无符号整数。所以在比较大小的时候需要先将其转换成 i n t int int类型,使用强制类型转换即可,否则根据程序设计中的特点,可能会出现一些意外的错误,这里列举一种,例如 i n t int int类型中的-1和 u n s i g n e d _ i n t unsigned\_int unsigned_int中的1进行比较,程序会先将两边的数字统一转换为 u n s i g n e d _ i n t unsigned\_int unsigned_int类型的整数,那么看着是-1,实际在比较的时候就是一个非常大的正数,也就是 2 32 − 1 2^{32}-1 232−1。故最终会得到“-1>1”的情况。
接下来对于该库的使用,在定义字符串时,可以直接使用 s t r i n g string string作为关键字来进行变量定义,而字符串中的某一个元素使用起来和字符数组基本没有差异。
KMP核心算法—next数组
对于
K
M
P
KMP
KMP算法来说,其最核心的部分就是其中的
n
e
x
t
next
next数组,首先抛开
K
M
P
KMP
KMP算法不谈,先讲讲什么是
n
e
x
t
next
next数组,这里先定义一个字符串
P
P
P,则其对应的
n
e
x
t
next
next数组的定义如下所示。
n
e
x
t
[
i
]
=
{
−
1
,
i
=
0
max
{
k
∣
0
<
k
<
i
且
P
0
P
1
.
.
.
P
k
−
1
=
P
i
−
k
.
.
.
P
i
−
1
}
next\left[ i \right] =\begin{cases} -1, i=0\\ \max \left\{ k|0<k<i\text{且}P_0P_1...P_{k-1}=P_{i-k}...P_{i-1} \right\}\\ \end{cases}
next[i]={−1,i=0max{k∣0<k<i且P0P1...Pk−1=Pi−k...Pi−1}
从上述公式中可以看出,
P
0
P
1
.
.
.
P
k
−
1
=
P
i
−
k
.
.
.
P
i
−
1
P_0P_1...P_{k-1}=P_{i-k}...P_{i-1}
P0P1...Pk−1=Pi−k...Pi−1中对应的
k
k
k值其实就是字符串
P
P
P的一个子串的最长相同前缀后缀,这个子串从
P
P
P的0位置开始,到
i
−
1
i-1
i−1处为止,故也可以理解为字符串
P
P
P的前
i
i
i个字符构成的字串。
通过上述分析也就对 n e x t next next数组的定义有了一个理解, n e x t next next数组记录的是字符串 P P P的不同前缀的最长相同前缀后缀的长度。其中 n e x t [ i ] next[i] next[i]的值为 P P P的前 i i i个字符构成的子串的最长相同前缀后缀。
接下来就要考虑如何计算这个
n
e
x
t
next
next数组,这里就需要使用到
K
M
P
KMP
KMP算法的思想,也就是使用前后缀匹配来进行重复过程的去除。以下图为例,
如上图所示,此时b和c不匹配,使用传统暴力法来求解的话,则是需要将方框中的两个字符进行比较,也就是于原字符串后移一个字符,然后模式串需要从头开始进行比较判断。
但是仔细观察该例子可以看出,这样匹配下去是没有用的,中间部分依旧匹配不上。这里就可以利用前面的最长前缀后缀的思想,例如在这里,下面的字符串匹配到字符c才出现不匹配,这说明c之前的字符全部都匹配上了。那么如果已经知道了c字符处的 n e x t next next数组值,可以看出就是1,那么接下来直接将 P [ 1 ] P[1] P[1]和 Q Q Q中的b字符(当前不匹配的字符)进行比较即可。
那么为什么上述方法就可以呢?仔细思考
n
e
x
t
next
next数组的定义,以这个具体例子为例,c处的
n
e
x
t
next
next数组值为1,说明abca这个字符串的最长前缀后缀长度为1,即前缀a=后缀a,而后缀a和原字符串当前匹配的b字符之后的a字符是匹配的,那么使用
n
e
x
t
next
next数组值,即将
P
[
1
]
P[1]
P[1]和
Q
Q
Q中的b字符进行比较时,此时的
P
[
0
]
=
a
P[0]=a
P[0]=a是和b字符前面的a字符是匹配的。
在上述例子中,设第一个不匹配的字符在模式串 P P P中位置为 i i i,那么根据 n e x t next next数组可以知道 P P P中前 n e x t [ i ] next[i] next[i]个字符和后 n e x t [ i ] next[i] next[i]个字符是相同的,而后 n e x t [ i ] next[i] next[i]个字符是和原字符串 Q Q Q中当前不匹配的字符前面的 n e x t [ i ] next[i] next[i]个字符是相匹配的,故前面 n e x t [ i ] next[i] next[i]的内容和原字符串 Q Q Q中当前不匹配的字符前面的 n e x t [ i ] next[i] next[i]个字符是相匹配的,所以接下来只需要将之前不匹配的字符和 P [ n e x t [ i ] ] P[next[i]] P[next[i]]进行比较即可,即 i = n e x t [ i ] i=next[i] i=next[i]。
在知道了
n
e
x
t
next
next数组有什么用以及
K
M
P
KMP
KMP算法的思想之后,接下来就可以利用上述思想来计算
n
e
x
t
next
next数组的值。首先
n
e
x
t
[
0
]
=
−
1
next[0]=-1
next[0]=−1,也可以等于0,这个只是不同的写法而已,这里统一规定
n
e
x
t
[
i
]
=
−
1
next[i]=-1
next[i]=−1。接下来根据定义即可,假设已知
i
i
i位置处的
n
e
x
t
[
i
]
=
k
next[i]=k
next[i]=k,即
P
0
P
1
.
.
.
P
k
−
1
=
P
i
−
k
.
.
.
P
i
−
1
P_0P_1...P_{k-1}=P_{i-k}...P_{i-1}
P0P1...Pk−1=Pi−k...Pi−1,那么很明显,如果
P
[
k
]
=
P
[
i
]
P[k]=P[i]
P[k]=P[i],那么就可以得到
n
e
x
t
[
i
+
1
]
=
k
+
1
next[i+1]=k+1
next[i+1]=k+1;反之,则需要使用前面的思想继续匹配,即
P
[
i
]
P[i]
P[i]和
P
[
n
e
x
t
[
k
]
]
P[next[k]]
P[next[k]]进行比较,如果相同,则
P
[
i
]
=
n
e
x
t
[
k
]
+
1
P[i]=next[k]+1
P[i]=next[k]+1。
如果直到遇到 n e x t [ k ] = − 1 next[k]=-1 next[k]=−1,那么说明此处没有最长相同前缀后缀,则原字符串从不匹配的下一个字符进行计算,模式串从0开始。代码如下。
// 获取next数组
void getNext(int* next, string p)
{
int j = 0; // 从0开始
int k = -1; // next[0] = -1
next[j] = k;
while (j < (int)p.size() - 1) // 逐个字符进行比对
{
if (k == -1 || p[j] == p[k]) // 匹配成功或最长相同前缀后缀为0
{
j++;
k++;
next[j] = k;
}
else // 匹配失败
k = next[k];
}
}
KMP匹配
在得知了 n e x t next next数组如何计算之后,其实 K M P KMP KMP算法中的过程也就基本出来了,也是默认开始时逐个字符的去进行比较,在遇到不匹配的情况时,使用 n e x t next next数组去更新下一次去匹配的位置,这样的好处在于不需要将原字符串 Q Q Q的指针进行回溯,即只需要遍历一次 Q Q Q字符串即可。
// KMP算法
int KMP(string p, string q)
{
int* next = new int[(int)p.size()]; // 定义next数组
getNext(next, p); // 获取next数组
int i = 0; // 模式串p的下标
int j = 0; // 原字符串q的下标
while (i < (int)p.size() && j < (int)q.size()) // 逐个匹配即可
{
if ( i == -1 || p[i] == q[j]) // 如果匹配成功或p无法与q当前字符匹配,则跳过
{
i++;
j++;
}
else // 匹配失败则用next数组更新
i = next[i];
}
delete []next;
if (i == (int)p.size()) // 如果i走到了末尾则说明匹配成功
return j - i;
return -1; // 说明匹配失败
}
next数组计算例题
完成了上述过程即算是基本对于KMP算法有了一个基本的了解,接下来对于
n
e
x
t
next
next数组的计算进行一个巩固。
接下来对该问题进行解答
KMP算法改进
接下来对 K M P KMP KMP算法进行一个简单的改进,主要在 n e x t next next数组的计算上面进行一些简单的改进。首先还是看看为什么可以进行改进,以下面这个例子为例。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mPRjbklG-1627655510996)(…\4.串\pics\fd77685cc76eddad2c3d328fc8663ec.png)] 从上面简单看出,由于在模式串中可能存在 P [ k ] = P [ n e x t [ k ] ] P[k]=P[next[k]] P[k]=P[next[k]]的情况,如果 P [ k ] ! = P [ j ] P[k]!=P[j] P[k]!=P[j],那么其实使用 k = n e x t [ k ] k=next[k] k=next[k]中会做一些无用功,那么有效的做法就是将所有 P [ k ] = P [ n e x t [ k ] ] P[k]=P[next[k]] P[k]=P[next[k]]的next值进行统一。故整个流程如下所示。
-
首先计算出next数组
-
遍历一次字符串,比较 P [ i ] = P [ n e x t [ i ] ] P[i]=P[next[i]] P[i]=P[next[i]]
2.1 如果相等,则 n e x t v a l [ i ] = n e x t v a l [ n e x t [ i ] ] nextval[i]=nextval[next[i]] nextval[i]=nextval[next[i]]。
2.2 如果不相等,则 n e x t v a l [ i ] = n e x t [ n e x t [ i ] ] nextval[i]=next[next[i]] nextval[i]=next[next[i]]。
接下来对于
n
e
x
t
v
a
l
nextval
nextval数组的计算进行一个简答的练习。