百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

1个公式就弄懂了KMP模式匹配算法,困扰我多年的问题终于解决了

wxin55 2024-11-17 16:46 8 浏览 0 评论

KMP算法可以说是一个很经典的模式匹配算法了,它一种改进的字符串匹配算法,但很多人就是不理解,甚至多看几次之后也没有理解透彻。

我们要查找S字符串串中是否包含P字符串,将P串称之为模式匹配串(以下简称模式串)。

朴素模式串匹配算法

我们先用一个动图来看不用KMP匹配的 朴素模式串匹配算法:

浅显易懂吧!但是呢,朴素的模式串匹配算法是有效率问题的!比如像下面这种:

在匹配到第5个字符时,发生了P串和S串不匹配的情形,然后按照朴素的算法,需要回溯 i 指针到 1 位置,回溯 j 指针到 0 位置,然后我们依次移动P串进行匹配计算:

在i = 3时,此时后面就有S[3] = P[0], S[4] = P[1], S[5] = P[2]:

然后S[6] != P[3]:

才发现又是不匹配的,其中我们可以用肉眼观察到,当S[1]和P[0]发生不匹配时,P串依次的从S串第1个位置,第2个位置进行比较,只有到S串第3个位置字符是A时才和P串的第0个字符匹配,而且后面的两个字符也能够匹配,但是,S串第5个字符是不匹配的。

假如我们是通过肉眼判断的话,当

这时不匹配时,我们应该直接移动 j 到

进行匹配判断,因此,朴素的模式匹配是有一部分匹配判断是多余的,我们要想办法把它优化,在S[5] != P[5] 时,直接进行判断 S[5] 是否等于 P[2]。这是因为P[0] == P[3], P[1] == P[4];P串前后有相同的子串,可以看下图知道:

由于S[3] = P[3], S[4] = P[4],而 P串前后有相同的子串:P[0] == P[3], P[1] == P[4],因此,我们可以直接进行判断S[5] 是否 等于 P[2]。也就是说当S[i] 和 P[j] 不匹配时,我们可以决定 j 的下一个位置是什么,从而使 i 指针不产生回溯。

推广到一般情况,我们可以用数学来证明这个事情:

这就有了KMP算法,我们假设S串下标是i,P串下标是 j。当P串中存在重合的子串时,我们需要分析出j指针指向的下一个位置,为了记录所有 j 取值的情况,用next数组来记录每一个P串的字符不匹配时需要将 j 指针移动到的位置下标。

KMP算法核心

核心思想:模式串中有重合的子串,那么当模式串p[j]匹配失败时,要移动到下标为k的位置进行匹配,k需要满足:0 ~ k 之间的 k+1 个字符与 j-1-k ~ j-1 之间的 k+1 个字符相同。next数组用于存放匹配失败时,next[j]代表需要进行匹配下一个字符的位置p[next[j]]。

可以用数学语言表述为:

模式串p中有n个字符,其中

如果存在

使得

那么令

否则(p中没有字符相同),令

说明:

  • 当 k = 0 时,表示模式串p中前后只有一个字符能够重合,此时要从p[0](从头开始)匹配;
  • 当 k = -1 时,表示模式串中没有可以前后重合的部分子串,此时需要将原始串的指针下标(i)后移。

记住:k = next[j]; 当 s[i] != p[j]时,需要将j指针移动到p串的k下标位置,那么next[j]就用来存储这个k。

具体解释:

  • 第一种情况:j == 0时,s[i]和p[j]不匹配,那j指针不可能再向左移动了,此时应该要i指针向后移动。这种情况,记next[j] = -1;
  • 第二种情况:j == 1时, s[i]和p[j]不匹配,那j指针显然是要移动到0位置的。这种情况,记next[j] = 0;
  • 第三种情况:p[j] = p[k] 时,有 next[j+1] = next[j] + 1;

证明:

因为p[j]之前有 p[j-k ~ j-1] = p[0 ~ k-1]; 所以 next[j] = k;

那此时有 p[j] = p[k];可以得到 p[j-k ~ j-1] + p[j] == p[0 ~ k-1] + p[k];

即:p[j-k ~ j] == p[0 ~ k];

那么 next[j+1] = k + 1 = next[j] + 1;

  • 第四种情况:p[j] != p[k] 时,此时应该是 k = next[k];

代码中为了保证循环依次计算j之前的重合子串,用j+1表示当前匹配不成功的字符位置,则计算next数组的算法如下:

void getNext(char *p, int next[])
{
    int j = 0, k = -1, len = strlen(p);
    if (len == 0)
    return;
    next[0] = -1;
    while (j < len-1)
    {
        if (k == -1 || p[j] == p[k])
            next[++j] = ++k; //next[j+1]记录p[j+1]匹配失败时需要跳转的位置
        else
            k = next[k];
    }
}

由此可以写出KMP算法:

int KMP(char s[], char p[])
{
    int[] next = getNext(p);
    int i = 0, j = 0;
    while (s[i] && p[j])
    {
        if (j == -1 || s[i] == p[j])
            i++, j++;
        else
        j = next[j]; // j回到指定位置
    }
    if (p[j])
        return -1;
    return i - j;
}

改进的KMP算法

回顾第三种情况:p[j] = p[k] 时,有 next[j+1] = next[j] + 1;

当s[i] != p[j]时,需要将 j 移动到 next[j],然而由于 p[j] == p[next[j]],导致移动j之后 s[i] 和 p[next[j]]仍然不匹配,此时需要继续将j移动到next[next[j]]进行比较;

显然,当p[j] 与 s[i]不匹配时,若p[j] == p[next[j]],就可以跳过j移动到next[j]这一步,直接进行移动到下一步。

因此,修正后的nextval数组如下:

改进思想:

在保证

p[i] = p[j - k + i],(i = 0, 1, ..., k)

后,再继续检查 p[j] 是否与 p[k] 相等?(此时原本是令 next[j] = k)

因为此时 k 记录的是 p[j] 发生不匹配时,需要直接跳到下次比较的位置;

所以:

  • 如果 p[k] = p[j] ,则同样发生不匹配,这时应该令 next[j] = next[k](因为 p[k] 与 p[j] 相同,则当前字符 p[j] 不匹配时,p[k]同样也不匹配,next[k]记录的是 p[k] 不匹配时需要跳转的位置,因此 p[j] 不匹配应该直接跳转到 next[k] 位置进行比较);
  • 如果 p[k] != p[j],那么当 p[j] 不匹配时才应该跳转到 p[k] 再次进行匹配,此时应令 next[j] = k。

改进算法如下:

void getNextval(char p[], int nextval[])
{
    int j = 0, k = -1, len = strlen(p);
    if (len == 0)
        return;
    nextval[j] = k;
    while (j < len-1)
    {
        if (k == -1 || p[j] == p[k])
        {
            ++j, ++k;
            if(p[j] != p[k])
                nextval[j] = k;
            else
            nextval[j] = nextval[k];
        }
        else
            k = nextval[k];
    }
}

kmp匹配的算法代码如下:

/**
* kmp模式匹配子串
* 参数:
* *str - 源串
* *substr - 目标串
*/
int kmp(char *str, char *substr)
{
    int i=0, j=0, slen = strlen(str), plen = strlen(substr);
    int *nextval = (int *) calloc(plen, sizeof(int));
    getNextval(substr, nextval);
    while(i < slen && j < plen)
    {
        if( j == -1 || str[i] == substr[j])
            i++, j++;
        else
            j = nextval[j];
    }
    if(j == plen)
        return i - j;
    else
        return 0;
}

如果觉得这篇文章对你有帮助,就帮我点赞并转发吧!我会持续分享IT、科技、技术方面的知识、经验,如果你喜欢的话,可以加个关注收藏一下吧!@IT研究僧大师兄

相关推荐

ES6中 Promise的使用场景?(es6promise用法例子)

一、介绍Promise,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码doSomething(f...

JavaScript 对 Promise 并发的处理方法

Promise对象代表一个未来的值,它有三种状态:pending待定,这是Promise的初始状态,它可能成功,也可能失败,前途未卜fulfilled已完成,这是一种成功的状态,此时可以获取...

Promise的九大方法(promise的实例方法)

1、promise.resolv静态方法Promise.resolve(value)可以认为是newPromise方法的语法糖,比如Promise.resolve(42)可以认为是以下代码的语...

360前端一面~面试题解析(360前端开发面试题)

1.组件库按需加载怎么做的,具体打包配了什么-按需加载实现:借助打包工具(如Webpack的require.context或ES模块动态导入),在使用组件时才引入对应的代码。例如在V...

前端面试-Promise 的 finally 怎么实现的?如何在工作中使用?

Promise的finally方法是一个非常有用的工具,它无论Promise是成功(fulfilled)还是失败(rejected)都会执行,且不改变Promise的最终结果。它的实现原...

最简单手写Promise,30行代码理解Promise核心原理和发布订阅模式

看了全网手写Promise的,大部分对于新手还是比较难理解的,其中几个比较难的点:状态还未改变时通过发布订阅模式去收集事件实例化的时候通过调用构造函数里传出来的方法去修改类里面的状态,这个叫Re...

前端分享-Promise可以中途取消啦(promise可以取消吗)

传统Promise就像一台需要手动组装的设备,每次使用都要重新接线。而Promise.withResolvers的出现,相当于给开发者发了一个智能遥控器,可以随时随地控制异步操作。它解决了三大...

手写 Promise(手写输入法 中文)

前言都2020年了,Promise大家肯定都在用了,但是估计很多人对其原理还是一知半解,今天就让我们一起实现一个符合PromiseA+规范的Promise。附PromiseA+规范地址...

什么是 Promise.allSettled()!新手老手都要会?

Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的pr...

前端面试-关于Promise解析与高频面试题示范

Promise是啥,直接上图:Promise就是处理异步函数的API,它可以包裹一个异步函数,在异步函数完成时抛出完成状态,让代码结束远古时无限回掉的窘境。配合async/await语法糖,可...

宇宙厂:为什么前端离不开 Promise.withResolvers() ?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发。1.为什么需要Promise.with...

Promise 新增了一个超实用的 API!

在JavaScript的世界里,Promise一直是处理异步操作的神器。而现在,随着ES2025的发布,Promise又迎来了一个超实用的新成员——Promise.try()!这个新方法简...

一次搞懂 Promise 异步处理(promise 异步顺序执行)

PromisePromise就像这个词的表面意识一样,表示一种承诺、许诺,会在后面给出一个结果,成功或者失败。现在已经成为了主流的异步编程的操作方式,写进了标准里面。状态Promise有且仅有...

Promise 核心机制详解(promise机制的实现原理)

一、Promise的核心状态机Promise本质上是一个状态机,其行为由内部状态严格管控。每个Promise实例在创建时处于Pending(等待)状态,此时异步操作尚未完成。当异步操作成功...

javascript——Promise(js实现promise)

1.PromiseES6开始支持,Promise对象用于一个异步操作的最终完成(包括成功和失败)及结果值的表示。简单说就是处理异步请求的。之所以叫Promise,就是我承诺,如果成功则怎么处理,失败怎...

取消回复欢迎 发表评论: