KMP算法解析

一、问题提出

我们在使用这一数据结构时,模式匹配算法是不可避免的,即给定一个小的字符串(称为模式串),返回其在主串第一次出现的位置。例如:模式串“abb”在主串“cacabbabbcb”第一次出现的位置是4,匹配成功;而模式串“abb”在主串“cacabcaabcb”中根本没有出现,因此也称“匹配失败”。
那么,选择一个快速高效的匹配算法就显得尤为重要。


二、常规思想(暴力匹配)

一般来说,我们对于匹配问题的传统思想就是“暴力匹配法”,即一个一个从头开始试直到匹配成功,这是一个很自然的思路,因此也就有对应的“BF(Brute-Force)算法”。描述如下:

i. 将模式串第一个字符主串第一个字符比较,如果匹配,再比较模式串第二个字符主串第二个字符,如果匹配,以此类推;

ii. 如果发现模式串有某个字符与主串相应字符不匹配,则将模式串右移一位,然后重新从模式串第一个字符开始继续与主串对应位置比较;

iii. 重复i和ii的过程直到模式串最后一个字符匹配成功,整个字串匹配成功并返回匹配成功的相应位置;

iv. 若到主串最后一个字符都没有匹配成功,则匹配失败。

下面举个例子,主串为“cacacbab”,模式串为“acb”。

第一趟:

位置:    1 2 3 4 5 6 7 8
主串:    c a c a c b a b
模式串: a c b
模式串的第一个字符“a”与主串的第一个字符“c”比较,不匹配,将模式串右移一个位,重新比较;

第二趟:

位置:    1 2 3 4 5 6 7 8
主串:    c a c a c b a b
模式串:   a c b
模式串的第一个字符“a”与主串的第二个字符“a”比较,匹配,继续比较;

位置:    1 2 3 4 5 6 7 8
主串:    c a c a c b a b
模式串:   a c b
模式串的第二个字符“c”与主串的第三个字符“c”比较,匹配,继续比较;

位置:    1 2 3 4 5 6 7 8
主串:    c a c a c b a b
模式串:   a c b
模式串的第三个字符“b”与主串的第四个字符“a”比较,不匹配,将模式串右移一个位,重新比较;

第三趟:

位置:    1 2 3 4 5 6 7 8
主串:    c a c a c b a b
模式串:      a c b
模式串的第一个字符“a”与主串的第三个字符“c”比较,不匹配,将模式串右移一个位,重新比较;

第四趟:

位置:    1 2 3 4 5 6 7 8
主串:    c a c a c b a b
模式串:         a c b
模式串的第一个字符“a”与主串的第四个字符“a”比较,匹配,继续比较;

位置:    1 2 3 4 5 6 7 8
主串:    c a c a c b a b
模式串:         a c b
模式串的第二个字符“c”与主串的第五个字符“c”比较,匹配,继续比较;

位置:    1 2 3 4 5 6 7 8
主串:    c a c a c b a b
模式串:         a c b
模式串的第三个字符“b”与主串的第六个字符“b”比较,匹配,继续比较;

位置:    1 2 3 4 5 6 7 8
主串:    c a c a c b a b
模式串:         a c b _
这时发现模式串已经结束了,至此,模式串全部字符均对应上了,匹配成功,并返回其在主串的位置4.

:在以上讲解算法的部分,“将模式串右移一个位”只是为了方便说明。实际上,串的位置肯定是不会动的,动的只是指针(即i、j),在程序中也是这样,是通过指针的移动进而控制元素之间的对比(详见代码部分)。


三、改进的模式匹配算法——KMP算法

1、算法思想

对于BF暴力匹配算法来讲,如果模式串恰好为主串的后缀的话,每趟匹配都需要一一把模式串所有字符与主串比较,会产生很多无用的“回退”,比如,主串为aaaaac,模式串为ac(请读者自行用BF算法测试)。但实际上,中间很多比较都是做的无用功,完全可以省去,下面的KMP算法就解决了这个问题。在BF算法的第二趟

主串:    c a c a c b a b
模式串:   a c b

比较时,“b”与“a”不匹配,若按照暴力算法,下一步应该是模式串重新将“a”与主串中第三个字符“c”进行比对,即

主串:    c a c a c b a b
模式串:      a c b

而事实上,只需将模式串中的“a”直接与主串中产生失配的地方“a”比对即可,而无需再回退到主串某个位置,即

主串:    c a c a c b a b
模式串:         a c b

这样,对于主串来说,比较顺序是一直向前进行的,没有产生任何“回退”,这就是KMP算法的中心思想。


2、算法流程

有了上述思想之后,我们来看下在执行该算法时时应该考虑的流程。

(1) 首先需要介绍前缀后缀的概念:

对于一个字符串,其前缀是指其所有头部子串(包括本身)构成的集合,而“真前缀”就是不包括其本身的所有头部子串构成的集合,可以参考子集真子集的比较。同样,后缀是指其所有尾部子串(包括本身)构成的集合,而“真后缀”就是不包括其本身的所有尾部子串构成的集合,注意,不论前缀还是后缀,其字符排列顺序都是从左至右,与原串相同,下面举例说明:

对于串“abacab”,其前缀是{a, ab, aba, abac, abaca, abacab},真前缀是{a, ab, aba, abac, abaca};其后缀是{abacab, bacab, acab, cab, ab, b},真后缀是{bacab, acab, cab, ab, b}.

(2) 接下来,是最长相等真前后缀长度的概念:

最长相等真前后缀长度即某串真前缀真后缀交集后,集合中最长的串的长度,以串“ababa”为例:

对于串“ababa”,其真前缀{a, ab, aba, abab}与真后缀{baba, aba, ba, a}的交集为{a, aba},其中“aba”最长,为3,因此串“ababa”的最长相等真前后缀长度为3

(3) KMP算法流程:

这里先不具体解释第“(1)、(2)”步到底在干什么,因为很难理解,等按照以下步骤,再加上后面的实例走一遍,就差不多可以理解了。

i. 计算模式串所有前缀的最长相等真前后缀长度;
ii. “i.”中所有长度构成部分匹配值(PM)数组,每一个值对应一个字符;
iii. 部分匹配值按位右移,左边用-1补齐,再统一加1,得到Next数组
iv. 在匹配过程中,如果在模式串的某个字符出现失配,以该字符对应的Next值跳到模式串相应位置,再与主串当前位置进行比较;
iv. 重复以上过程直至完全匹配成功或者匹配失败,结束程序。

听起来很难理解,我们还是看一个具体的例子,假如主串为“bacbacbcacbac”,模式串为“acbcacb”

i. 找到模式串的所有前缀:
“a”、“ac”、“acb”、“acbc”、“acbca”、“acbcac”、“acbcacb”,然后分别计算它们的最长相等真前后缀长度:

“a”的真前缀为∅,后缀为∅,交集为∅,故最长相等真前后缀长度为0;

“ac”的真前缀为{a},真后缀为{b},交集为∅,故最长相等真前后缀长度为0;

“acb”的真前缀为{a, ac},真后缀为{cb, b},交集为∅,故最长相等真前后缀长度为0;

“acbc”的真前缀为{a, ac, acb},真后缀为{cbc, bc, c},交集为∅,故最长相等真前后缀长度为0;

“acbca”的真前缀为{a, ac, acb, acbc},真后缀为{cbca, bca, ca, a},交集为{a},故最长相等真前后缀长度为1;

“acbcac”的真前缀为{a, ac, acb, acbc, acbca},真后缀为{cbcac, bcac, cac, ac, c},交集为{ac},故最长相等真前后缀长度为2;

“acbcacb”的真前缀为{a, ac, acb, acbc, acbca, acbcac},真后缀为{cbcacb, bcacb, cacb, acb, cb, b},交集为{acb},故最长相等真前后缀长度为3;

ii. 部分匹配值即以上全部最长相等真前后缀长度的集合,{0, 0, 0, 0, 1, 2, 3},得到部分匹配值表

在这里插入图片描述

iii. 计算Next数组
在这里插入图片描述
在这里插入图片描述

这里,Next[1]始终等于0,即规定:如果在模式串的第一个位置就发生了失配,直接将模式串后移1位,再跟主串匹配,以此类推;

下面,给出Next数组的计算公式:
若T[j]表示模式串T的第j个位置的字符,则

① j = 1 时,Next[j] = Next[1] = 0;

② Next[j] = Max{k},此时需满足1 < k < j 且模式串的子串“T[1: k-1]” = “T[j-k+1: j-1];

③ Next[j] = 1,其他情况。

iv. 开始模拟匹配过程

第一趟:

位置:    1 2 3 4 5 6 7
主串:    b a c b a c b c a c b a c
模式串:a c b c a c b
模式串的第一个字符“a”与主串的第一个字符“b”比较,不匹配,将模式串右移一个位,重新比较;

第二趟:

位置:    1 2 3 4 5 6 7
主串:    b a c b a c b c a c b a c
模式串:   a c b c a c b
模式串的第一个字符“a”与主串的第二个字符“a”比较,匹配,继续比较;

位置:    1 2 3 4 5 6 7
主串:    b a c b a c b c a c b a c
模式串:   a c b c a c b
模式串的第二个字符“c”与主串的第三个字符“c”比较,匹配,继续比较;

位置:    1 2 3 4 5 6 7
主串:    b a c b a c b c a c b a c
模式串:    a c b c a c b
模式串的第三个字符“b”与主串的第四个字符“b”比较,匹配,继续比较;

位置:    1 2 3 4 5 6 7
主串:    b a c b a c b c a c b a c
模式串:    a c b c a c b
模式串的第四个字符“c”与主串的第五个字符“a”比较,不匹配,这时找到Next[4] = 1,跳到模式串的第一个字符,即“a”处,重新跟主串目前位置比较;

第三趟:

位置:    1 2 3 4 5 6 7
主串:    b a c b a c b c a c b a c
模式串:             a c b c a c b
模式串的第一个字符“c”与主串的第五个字符“a”继续比较(而不是跟第三个字符“c”比较),匹配,继续比较;

以此类推,模式串的“c”与主串的“c”比较,匹配;

“b”与“b”比较,匹配;

……

最后一个字符“b”与“b”也匹配,模式串已经遍历结束,匹配成功。

可以看到,在KMP算法中,前两趟比较与暴力算法BF一致,但是在第三趟时,暴力算法仍然会将模式串的第一个字符“a”与主串的第三个字符“c:比较,然后继续第四趟、第五趟,而KMP算法直接跳过了这两趟,直接将模式串的“a”与主串第五个字符“a”比较,从而节约了很多不必要的步骤,节约了时间。


四、KMP算法进一步改进

试想,若主串为“aacaaabac”,模式串为“aaaaab”,其Next数组为
在这里插入图片描述
当进行到如下步骤时:

位置:    1 2 3 4 5 6 7 8 9
主串:    a a c a a a b a c
模式串:a a a a a b

模式串的第三个字符“a”与主串的第三个字符“c”发生失配,此时应当跳到第Next[3] = 2个字符即第二个“a”处开始比较,

位置:    1 2 3 4 5 6 7 8 9
主串:    a a c a a a b a c
模式串:   a a a a a b

发生失配,然后,再跳到Next[2] = 1即第一个“a”处进行比较,

位置:    1 2 3 4 5 6 7 8 9
主串:    a a c a a a b a c
模式串:      a a a a a b

但很明显,由于这两次都是模式串中的“a”跟主串中的“c”进行比较,是不必要的,这是因为发生了T[j] = T[Next[j]],即发生失配的字符跟下一个即将比较的字符是相等的字符,因此我们只需令Next[j] = Next[Next[j]]即可,如果依然是同一个字符,以此类推,直至下一个待匹配的字符不是当前失配的字符。
也就是说,改进后的KMP算法,不仅避免了BF算法下的“回退”现象,也能避免模式串中的重复比较,进一步加快了效率。


五、总结

在第“三、”部分结尾时,说道,前几次的比较BF与KMP是一致的,而在第三趟匹配时,KMP跳过了无用功,这是因为,主串与子串中有部分是相同的,即

主串:    b a c b a c b c a c b a c
模式串:    a c b c a c b

可以看到,在“c”与“a”进行比较时,前面部分三个字符串“acb”是相同的。那么我们可以这样想:既然模式串的前三个字符与主串中三个字符相同,即“部分匹配”,但恰恰在后面的一位字符处发生了失配,那么模式串跟主串完全匹配的地方肯定位于主串这三个“部分匹配”的字符后面的部分,因此我不能再重新将模式串从头开始与主串的下一个字符比较了,而是直接跳过主串的这三个“部分匹配”串,从后面再开始比较,从而节约了很多无用的比较过程。

此外,在第“四、”部分也说道了,如果模式串中发生失配的字符跟按照传统KMP算法“应该”跳转到的下一个字符相同,也应该跳过,即“避免重复比较”,从而,再一次节省了不必要的步骤。

通过以上分析,若主串长度为M,模式串长度为N,可以得到暴力匹配BF算法的时间复杂度近似为 O(M+N) 。在极端情况下,即模式串是主串的后缀的情况,比如“aaaaaaac”和“aac”,这个时候,共需要比较 (M - N + 1)*N 次,当M很大时,就近似于 O(MN) 的时间复杂度。而对于KMP算法,时间复杂度即为 O(M+N) .

这里补充一点,KMP算法中Next数组的计算方法仅仅跟模式串有关,因为只有与主串出现“部分匹配的情况下”KMP算法才奏效,或者说,KMP算法就是基于模式串在匹配中的特点进行设计的。


六、代码实现

*注:此处使用Python编写,因为电脑就只装了Python,懒得安装C++编译环境了,就为了实现算法,大致步骤还是差不多的。
另外,Next数组在实际编程时的计算过程

1、串的简单定义
class String:
    def __init__(self):
        # 构造函数,并设置私有变量
        self.__ch = []  # 字符数组
        self.__length = 0  # 串长度

    def StrAdd(self, content):
        # 添加操作,将字符数组content内容增加给串
        for c in content:
            self.__ch.append(c)
            self.__length += 1

    def GetElem(self, i):
        # 获取串某位元素
        return self.__ch[i - 1]  # 串的第1个字符在数组中表现为下标0

    def PrintStr(self):
        # 输出串
        print(self.__ch)

    def StrLength(self):
        return self.__length

2、BF算法
def StrMatch_BF(S, T):
    i = j = 1  # i遍历主串S,j遍历模式串T
    while i <= S.StrLength() and j <= T.StrLength():
        if S.GetElem(i) == T.GetElem(j):
            # 字符相匹配,继续比较下一个字符
            i += 1
            j += 1
        else:
            # 字符不匹配,模式串从头开始与主串下一个位置比较,主串的下一个位置是i-j+2
            i = i - j + 2
            j = 1
    if j > T.StrLength():
        # 模式串遍历完毕,匹配成功
        return i - T.StrLength()
    else:
        # 匹配失败
        return -1

3、传统KMP算法
def GetNext(T):
    Next = [0]*(T.StrLength()+1)  # 相当于C++中的int Next[T.StrLength()+1],这里Next[0]空出来没用
    i = 1
    j = 0  # 注意区分串的下标和数组的下标(看串类的定义)
    while i < T.StrLength():
        if j == 0 or T.GetElem(i+1) == T.GetElem(j+1):
            i += 1
            j += 1
            Next[i] = j
        else:
            j = Next[j]
    return Next
def StrMatch_KMP(S, T, Next):
    i = j = 1
    while i <= S.StrLength() and j <= T.StrLength():
        if j == 0 or S.GetElem(i) == T.GetElem(j):
            # 字符相匹配,继续比较下一个字符
            i += 1
            j += 1
        else:
            # 否则,跳到对应Next值处进行比较
            j = Next[j]
    if j > T.StrLength():
        return i - T.StrLength()
    else:
        return -1

4、改进后的KMP算法
def GetNext_improved(T):
    Next = [0] * (T.StrLength() + 1)
    i = 1
    j = 0
    while i < T.StrLength():
        if j == 0 or T.GetElem(i+1) == T.GetElem(j+1):
            i += 1
            j += 1
            if(T.GetElem(i) != T.GetElem(j)):
                Next[i] = j
            else:
                Next[i] = Next[j]  # 避免重复比较
        else:
            j = Next[j]
    return Next
# KMP算法本身没变,只是Next数组的计算方法有些许改动
def StrMatch_KMP(S, T, Next):
    i = j = 1
    while i <= S.StrLength() and j <= T.StrLength():
        if j == 0 or S.GetElem(i) == T.GetElem(j):
            # 字符相匹配,继续比较下一个字符
            i += 1
            j += 1
        else:
            # 否则,跳到对应Next值处进行比较
            j = Next[j]
    if j > T.StrLength():
        return i - T.StrLength()
    else:
        return -1

5、代码测试与运行结果
def main():
    S, T = String(), String()
    S.StrAdd("cacacbab")
    T.StrAdd("acb")
    S.PrintStr()
    T.PrintStr()

    Next = GetNext(T)
    Next_improved = GetNext_improved(T)

    print("BF算法结果:", StrMatch_BF(S, T))
    print("传统KMP算法结果:", StrMatch_KMP(S, T, Next))
    print("改进KMP算法结果:", StrMatch_KMP(S, T, Next_improved))

if __name__ == "__main__":
    main()

以上代码运行结果为:

[‘c’, ‘a’, ‘c’, ‘a’, ‘c’, ‘b’, ‘a’, ‘b’]
[‘a’, ‘c’, ‘b’]
BF算法结果: 4
传统KMP算法结果: 4
改进KMP算法结果: 4

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度为O(m+n)。\[2\] 下面是一个使用KMP算法的C语言代码示例: ```c int KMP(char *chang, char *duan) { int c_strlen = strlen(chang); int d_strlen = strlen(duan); int c = 0, d = 0; while (c < c_strlen && d < d_strlen) { if (chang\[c\] == duan\[d\]) { c++; d++; } else { d = Next\[d\]; // 失配,指针回退到对应Next\[\]下标元素。 } } return d < d_strlen ? -1 : c - d; } ``` 在这段代码中,`chang`表示主串,`duan`表示模式串。算法通过比较主串和模式串的字符来进行匹配,当字符不匹配时,根据`Next`数组的值来决定模式串的指针回退位置。如果模式串完全匹配成功,则返回匹配的起始位置;否则返回-1。\[3\] 需要注意的是,代码中的`Next`数组没有给出具体实现,你需要根据KMP算法的原理来计算并填充`Next`数组。 #### 引用[.reference_title] - *1* [【详解】KMP算法——多图,多例子(c语言)](https://blog.csdn.net/Zero__two_/article/details/120334582)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [字符串匹配——KMP算法【C语言】](https://blog.csdn.net/weixin_45423515/article/details/124232548)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋之颂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值