模式匹配改进算法(KPM算法)原理讲解与代码实现(C,python)

 前言

 在学习数据结构过程中,本人碰到这个让人难懂的算法,看书看不进去,所以上博客来找找图解,发现自己还是不能够理解(原理不理解),最后还是选择把书啃下来。
由于本人水平有限,难免会出现一些错误。如果发现文章中存在错误还请批评指正,感谢您的阅读。
以下内容基于《数据结构(c语言版本)》还有其他博主的文章。

1.简单介绍KMP算法

KMP(Knuth-Morris-Pratt)算法是一种用于字符串匹配的经典算法,其效率远超于暴力匹配法,可以在O(n+m)的时间数量级上完成串的模式匹配。其精进在于:每趟匹配过程中出现字符串匹配不等时,不需要回溯指针,而是利用已经得到的匹配的部分,尽可能将模式串向右“滑动”尽可以远的距离,继续进行比较匹配。

1.1一些概念介绍

1.主串与模式串:

如图

2. 前缀

我们从左到右依次截取不同长度的子串,但不能包括完整的字符串(也就是不能取到最右边一个)。

例子字符串 abcdef

  • 长度为 1 的前缀是 a
  • 长度为 2 的前缀是 ab
  • 长度为 3 的前缀是 abc
  • 长度为 4 的前缀是 abcd
  • 长度为 5 的前缀是 abcde

所以字符串 abcdef 的真前缀包括:a, ab, abc, abcd, abcde

2. 后缀

我们从右到左依次截取不同长度的子串,但不能包括完整的字符串(也就是不能取到最左边一个)。

对于字符串 abcdef

  • 长度为 1 的后缀是 f
  • 长度为 2 的后缀是 ef
  • 长度为 3 的后缀是 def
  • 长度为 4 的后缀是 cdef
  • 长度为 5 的后缀是 bcdef

所以字符串 abcdef 的后缀包括:f, ef, def, cdef, bcdef

2.关于暴力匹配法

 这里不详细介绍,怎么个暴力法可以去看别的大佬文章,直接说缺点:

在暴力匹配算法中,主串和模式串逐个字符比较,若匹配失败,则主串向右移动一个字符继续匹配。这种方法的时间复杂度为 O(m*n),其中 m 为主串长度,n 为模式串长度。显然,对于长文本或多次匹配场景,这种算法效率极低。

3.部分匹配表(Partial Match Table, PMT)

KMP算法引入了部分匹配表,也称为失配函数(failure function)。部分匹配表存储了模式串中每个字符串的子串的最大相同前后缀的长度。

作用是:帮助模式串在匹配失败后快速定位到下一次可能匹配的位置

例:

a0没有前缀和后缀,部分匹配值为 0
aa1前缀和后缀都是 a,部分匹配值为 1
aab0没有相等的前缀和后缀,部分匹配值为 0
aaba1前缀是 a,后缀也是 a,部分匹配值为 1
aabaa2前缀是 aa,后缀也是 aa,部分匹配值为 2
aabaaf0没有相等的前缀和后缀,部分匹配值为 0
| 字符   | a  | a  | b  | a  | a  | f  |
|--------|----|----|----|----|----|----|
| PMT值  | 0  | 1  | 0  | 1  | 2  | 0  |

不知道大家好奇这是为什么不,为啥在这直接引入这个概念,而为啥这个表又能帮助模式串在匹配失败后快速定位到下一次可能匹配的位置?

本人当时也是因为这原因来找文章的,结果没得到自己想要的答案。

不着急,往下看

4.KMP算法原理

在《数据结构(c语言版本)》一书中给出了严谨的推导过程

以下是本人的理解

4.1问题引入

当主串中指针i指向的字符和模式串中指针k指向的字符比较不相等(也就是不匹配)时候,主串中i(位置不动)应该与模式串哪个字符再比较,也就是模式串应该向右滑行多远?

4.2问题分析

不妨假设他要和指针j=k的字符继续比较,而且不存在另外一个k`比k还大

这里列举具有代表性的模型(其他情况一样的)

细心的你这时候会发现
在i与j失配时候

s2 == s3

而在i与k匹配时候

s1 == s2

也就是在模式串中s1 == s3

聪明的你这时候想到:这不就是j前面的字符最大相同前后缀,就是为什么要引入部分匹配表这个概念,PMT值就是k的位置

(批注:s1和s3这两块区域是有可能相交的,k会在s3里面,但是为了方便演示,所以了采取这种情况)

接着,看下面这个例子

模式串的 “ f ” 与主串相应位置不匹配,那么我们就要看 “ f ” 前面的字符串最大相同前后缀,也就是查“ f ”前面的“ a ”的PMT值

接着往下对比配对即可。

4.3总结

KMP算法步骤就是构建部分匹配表和使用部分匹配表进行高效匹配

5.PMT函数(next函数)

接下来就是实现部分匹配表的函数,也叫next函数

我们将用递推方式实现,PMT[j] = k 表示j的PMT值为k

首先:PMT[0] = 0,这个好理解吧,一个字符没有公共前后缀,将其置为0

当j > 0时:如果我们已经知道PMT[j] = k,那么PMT[j+1]有多少种情况?

1.当j+1 == k+1时,那么PMT[j+1] = k +1 = PMT[j] +1

2.当j+1 != k+1时,这种情况有点头疼:我们得重新找他的最大相同前后缀

这时候有一个非常巧妙的方法

当你看到j+1 != k+1有没有想到什么?
就是主串和模式串失配的情况!

我们将他和自己当成主串和模式串,来进行模式匹配

接下来要做的事情就是,查k的PMT值,我们在前面说了这个过程是递推进行的,就是说PMT[k]的值是已知的了,就假设他为 l 吧

如果此时 j+1 == l,那么PMT[j+1] = PMT[k] +1

如果 j+1 != l,继续查 l 的PMT值......如果最后还是不相等,那么PMT[j+1] = 0

6.代码展示:

6.1:c实现

#include <stdio.h>
#include <string.h>

// PMT(部分匹配表)生成函数
void build_PMT(const char* pattern, int* pmt, int M) {
    int length = 0;                                         // 前缀长度
    pmt[0] = 0;                                             // PMT表的第一个值总是0

    for (int i = 1; i < M; i++) {
        while (length > 0 && pattern[i] != pattern[length]) {
            length = pmt[length - 1];                       // 回退到上一个最长前缀
        }
        if (pattern[i] == pattern[length]) {
            length++;
        }
        pmt[i] = length;                                    // 记录当前字符的最长前缀长度
    }
}

// KMP算法函数
void KMP(const char* text, const char* pattern) {
    int N = strlen(text);                                   // 文本长度
    int M = strlen(pattern);                                // 模式串长度

    int pmt[M];                                             // 创建PMT表
    build_PMT(pattern, pmt, M);

    int j = 0;                                              // 模式串中的索引
    for (int i = 0; i < N; i++) {
        while (j > 0 && text[i] != pattern[j]) {
            j = pmt[j - 1];                                 // 匹配失败,使用PMT表回退
        }
        if (text[i] == pattern[j]) {
            j++;
        }
        if (j == M) {
            printf("Found pattern at index %d\n",
                   i - M + 1);                               // 匹配成功
            j = pmt[j - 1];                                  // 寻找下一个可能的匹配(看需求)
        }
    }
}

int main() {
    const char* text1 = "aabaabaaf";
    const char* pattern1 = "aabaaf";
    KMP(text1, pattern1);
    printf("\n");
    const char* text2 = "aabaabaafaabaabaaf";
    const char* pattern2 = "aabaaf";
    KMP(text2, pattern2);
    return 0;
}

6.2:python实现

from typing import List
class Solution:
    def build_PMT(self,pattern:str) -> List[int]:
        """生成部分匹配表(PMT)"""
        M = len(pattern)
        pmt = [0] * M                       # PMT表初始为0
        length = 0                          # 当前最长前缀的长度

        # 从索引1开始计算PMT表
        for i in range(1, M):
            while length > 0 and pattern[i] != pattern[length]:
                length = pmt[length - 1]    # 回退到上一个最长前缀

            if pattern[i] == pattern[length]:
                length += 1
            pmt[i] = length                 # 记录当前字符的最长前缀长度

        return pmt


    def KMP(self,text:str, pattern:str) -> None:
        """KMP字符串匹配算法"""
        N = len(text)
        M = len(pattern)

        pmt = self.build_PMT(pattern)       # 生成PMT表
        j = 0                               # 模式串中的索引

        # 遍历主串
        for i in range(N):
            while j > 0 and text[i] != pattern[j]:
                j = pmt[j - 1]              # 匹配失败,使用PMT表回退

            if text[i] == pattern[j]:
                j += 1

            if j == M:                      # 找到一个匹配
                print(f"Found pattern at index {i - M + 1}")
                j = pmt[j - 1]              # 寻找下一个可能的匹配(看需求)


if __name__ == '__main__':
    fun = Solution()

    text1 = "aabaabaaf"
    pattern1 = "aabaaf"
    fun.KMP(text1, pattern1)
    print()
    text2 = "aabaabaafaabaabaaf"
    pattern2 = "aabaaf"
    fun.KMP(text2, pattern2)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值