KMP算法讲解

KMP介绍

来源:百度百科

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。

KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。

具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。

常规问题

给定一个字符串作为主串,再给定一个字符串作为模式串。

模式串可能在主串中重复出现,求每次出现的下标位置(主串下标从1开始)。

如果没有出现过,答案即为-1。

暴力解法

类似双指针、单调栈、单调队列,这些算法首先做出暴力解法,再进行优化。去掉一些不需要的情况判断(剪枝)。

int S[M], P[N]	// S是主串,P是模式串(子串)
// 暴力循环
for(int i = 1; i <= m; ++i) {
    for(int j = 1; j <= n; ++j) {
        // 如果存在不匹配的情况,则当前i不符合
        if(s[i + j - 1] != s[j])
            break;
    }
}

易知暴力解法时间复杂度较高,且有很多情况重复判断。

优化解法

匹配思路

KMP匹配的思路

S[M]是主串,P[N]是模式串。

  • 图中可以看到S[M]紫色区域是已经匹配过的区域。
  • 当前正在匹配的区域是黄色开始的,黄色区域为主串和模式串相同的区域。
  • 在红色区域,此时取单个字符,在遍历到这个字符时主串与模式串不等。
  • 绿色区域是没有开始匹配的区域。

主串与模式串匹配

此时发现不匹配的元素,移动模式串进行新的位置匹配。

暴力解法中,此时模式串会向后移动一位,从主串的下一个元素开始匹配,时间复杂度高。

此处进行优化,如果存在一个元素,模式串可以移动到这个元素处,进行下一轮的匹配,就可以节省很多情况。

具体见下图:

P[N]向后移动数个元素之后,

  • 此时蓝色区域的元素相等,则新一轮匹配可以从此处开始。即P[N]跳过了主串此时黄色区域的匹配情况,节省了时间。
  • 灰色区域是模式串未匹配区域。

模式串移动

接下来判断蓝色区域之后的元素是否匹配,遇到不匹配的元素则重复进行上述步骤,直到遍历完主串或者模式串匹配成功。


我们可以看到,模式串向后移动到的位置,即可以继续进行匹配的位置。这个距离,即next数组的元素。

next数组构建

对模式串每个元素构建next数组

next数组构建方法是将模式串自己与自己匹配,具体过程和KMP匹配过程类似。

声明几个概念:

  • "非平凡后缀":是指以当前字符串首字符开始,除去当前字符串中最后一个元素的所有子串情况;
  • "非平凡前缀":同上,指以当前字符串尾字符终止,除去当前字符串中第一个元素的所有子串情况;
  • "部分匹配值":指同一字符串中,非平凡后缀与非平凡前缀的最长公共子串。

next数组中的元素是"部分匹配值"的长度。构建过程举例:

模式串P[7] = "abcabca",下标从1开始

a b c a b c a
1 2 3 4 5 6 7
  1. next[1] = 0,易知为0;
  2. {a}——{b},next[2] = 0;
  3. {a},{ab}——{c},{bc},next[3] = 0;
  4. {a},{ab},{abc}——{a},{ca},{bca},next[4] = 1;
  5. {a},{ab},{abc},{abca}——{b},{ab},{cab},{bcab},next[5] = 2;
  6. {a},{ab},{abc},{abca},{abcab}——{c},{bc},{abc},{cabc},{bcabc},next[6] = 3;
  7. {a},{ab},{abc},{abca},{abcab},{abcabc}——{a},{ca},{bca},{abca},{cabca},{bcabca},next[7] = 4。

则构建next数组为

0 0 0 1 2 3 4

构建next数组是KMP算法的核心!

练习题目及代码

题目快照(题目来源acwing网站)

KMP练习题

完整代码

#include <iostream>
using namespace std;

const int N = 10010, M = 100010;
int n, m;
char p[N], s[M];
int next[N];	// 初始化为0

int main() {
    cin >> n >> p + 1 >> m >> s + 1;	// p,s均为字符数组的首地址指针
    // 构建next数组
    for(int i = 2, j = 0; i <= n; ++i) {	// next[1] = 0,故i从2开始
        while(j && p[i] != p[j + 1])
            j = next[j];
       	if(p[i] == p[j + 1])
            ++j;
        next[i] = j;
    }
    // KMP匹配过程
	for(int i = 1, j = 0; i <= m; ++i) {
        while(j && s[i] != p[j + 1])
            j = next[j];
        if(s[i] == p[j + 1])
            ++j;
        if(j == n) {	// 匹配成功
            // 本题要求返回每次成功匹配的起始下标位置
            cout << i - m << endl;	//(注意此时字符数组下标从1开始)
            j = next[j];		// 细节问题
        }
    }
    
    return 0;
}
posted @ 2023-05-12 00:30  菠菠吹雪  阅读(138)  评论(0)    收藏  举报