字符串匹配(KMP算法)

字符串匹配问题(KMP算法)

问题描述

​ 给定一个文本串S,现有一个模式串P,求P在S中出现的位置(第一次出现的位置)。

前提

​ 后文中会提到各种S串前进回退,P串前进回退,意指用于访问S串P串的指针或者迭代器的前进或者回退。后文为了简单,就直接写S串前进回退之类的了。

暴力匹配

​ 对于上述字符串匹配的问题,首先自然能想到的就是暴力匹配,以S中每个字符为起点,初始化一个迭代器进行循环,往后搜索是否后续元素一一与P中元素匹配,如果出现不匹配的情况,则S回退到本次搜索后续元素匹配情况的起点位置的下一个位置,P回退到首元素位置。代码自然也很简单,具体代码如下:

int violent_search(const char *s, const char *p)
{
    int sLen = (int) strlen(s);
    int pLen = (int) strlen(p);
    int i, j;
    i = j = 0;
    while (i < sLen && j < pLen) {
        if (s[i] == p[j]) {
            ++i, ++j;
        }
        else {  //S回退,P回退
            i = i - j + 1;
            j = 0;
        }
    }

    if (j == pLen) {
        return i - j;
    }
    else {
        return -1;
    }
}

​ 假定S串长度为n,P串长度为m,则计算时间复杂度时,首先遍历S串中每个元素为O(n),再根据S中每个元素与P首元素的匹配情况,遍历P串中每个元素为O(m),结果为O(n) * O(m) = O(m * n)。在P串远小于S串的情况下,这种方法效率不算很差,接近于O(n)。不过如果S串和P串长度差不多,时间复杂度就接近于O(n ^ 2)了。

暴力搜索的优化版本

​ 对于暴力搜索而言,简单直接,容易理解,但效率差这么一点。仔细观察上面的代码,可以发现一种情况:上面的版本将遍历S串中的每个元素,不管后续的元素是否有可能与P串匹配,举个例子,如果遍历S到倒数第三个元素,该元素与P串首元素相等,而P串长度为5,结果已经很明显了,S串后续元素是不可能与P串完全匹配的,遇到这种情况应该结束整个方法。具体代码如下:

int violent_search(const char *s, const char *p)
{
    int sLen = (int) strlen(s);
    int pLen = (int) strlen(p);
    int i, j;
    i = j = 0;
    while (i < sLen && j < pLen) {
        if (s[i] == p[j]) {
            ++i, ++j;
        }
        else {  //失配,回退S,回退P
            i = i - j + 1;
            j = 0;

            if (sLen - i < pLen) {  //若S串剩余未匹配元素数量小于P串长度,应结束方法
                break;
            }
        }
    }

    if (j == pLen) {
        return i - j;
    }
    else {
        return -1;
    }
}

​ 优化了这一步之后,这个方法要比O(m * n)要快一些,并且Linux Kernel中的strstr方法实现思路也是大致如此的。

KMP算法基本思路

​ 暴力匹配以及暴力匹配的优化版本,从思路、代码实现上都很容易理解,如果不吝啬于内存,想要更快的方法,自然也有解决方案,这就是KMP算法。暴力匹配两法都需要S串与P串的回退,KMP算法仅用回退P串,S串可以继续前进,这样特别的匹配方式是依靠next数组实现的,而整个KMP最难理解的也是next数组。下面就先来解释一下next的数组的意义吧。

​ next数组用于存放对应位置之前的子串存在多长的相同前缀后缀。举个例子,对于字符串”abcabcd”,数组以0起始,则’d’位置的next数值,即next[6]为3。解释一下,’d’之前的子串是”abcabc”,对于该子串,存在前缀”abc”与后缀”abc”相同,所以next[6]就被计算为前后缀的长度,即为3。

​ next数组存在的意义就在于此,暴力搜索及其优化版本都并未记录失配之前的匹配情况,所以每次失配都要造成S串与P串的回退,对于KMP算法,依靠next数组记录失配之前的匹配情况,失配后,根据next数组的记录,我们就只需要回退P串就行了,失配字符之前的子串已经与S串的子串匹配,也就是说失配之前的子串的后缀也已经与S串的子串的后缀匹配了,如此,根据next数组的数值,P串只需要回退到失配位置的next数组数值的位置即可,此时刚才失配时,P串失配字符之前的子串的前缀刚好与S串的子串的后缀匹配了!我们可以用一副示意图来理解一下这个过程:

《字符串匹配(KMP算法)》

​ 由上图可看出,当P串的’D’与S串的’E’匹配时,出现失配的情况。对于P串’D’之前,存在长度为3的相同的前后缀,所以可以将P回退到P[3]的位置,即第四个字符的位置继续与S串进行匹配。

​ 说到这里,也就大概解释了S串为什么不需要回退的原因,那S串什么时候前进呢?当P串的首元素也无法与S串失配位置匹配时,自然就需要S串前进一个位置了。

next数组计算方法

​ 假定有模式串P,长度为m,则P可表示为P[0]P[1]…P[m – 1]。重新定义一下next数组:

​ 若有P[0]P[1]…P[i] = P[j – i]P[j – i + 1]…P[j],则next[j + 1] = next[j] + 1 = i + 1。

由上可得求next数组时会出现的两种情况:

  1. P[i] = P[j],则next[j + 1] = next[j] + 1 = i + 1;
  2. P[i] != P[j],则判断P[next[i]] == P[j],若为真,则next[j + 1] = next[i] + 1,否则令i = next[i],继续执行查找匹配情况的循环。

解释:

​ 对于第2种情况需要说明一下,如果P[i] != P[j],转而去判断P[next[i]] == P[j],这是因为子串中并不存在相同的前缀P[0]P[1]…P[i]与后缀P[j – i]P[j – i + 1]…P[j],那么此时就该考虑是否存在一个更小的前缀P[0]…P[t]与更小的后缀P[j – t]…P[j]相等呢?所以对于这种情况,就应使用i = next[i],继续进行匹配。

​ 刚接触next数组的计算,可能会想一个问题,为什么P[i] != P[j]时,要用i = next[i]去进行迭代?这是因为P[i]!=P[j],next[i]表示P串的第i个字符之前的子串存在长度为next[i]的相同前缀后缀,即存在P[0]…P[next[i] – 1] = P[i – 1 – (next[i] – 1)]…P[i – 1], 此时只需判断P[next[i]]与P[j]是否相等,若两者相等,则存在P[0]…P[next[i]] = P[j – next[i]]…P[j],即找到了比P[0]…P[i]更小的前缀可与P[j – next[i]]…P[j]相匹配;若不相等,则继续以i = next[i]进行迭代。

下面即可得到求解next数组的代码了:

void get_next(const char *p, int next[])
{
    int pLen = (int) strlen(p);
    next[0] = -1;
    int i = -1;
    int j = 0;
    while (j < pLen - 1) {  //注意next数组元素数量与p串相同
        if (i == -1 || p[i] == p[j]) {
            ++i, ++j;
            next[j] = i;
        }
        else {
            i = next[i];
        }
    }
}

有了next数组,自然也能利用next数组求解匹配位置了,具体代码如下:

int kmp_search(const char *s, const char *p, int next[])
{
    int sLen = (int) strlen(s);
    int pLen = (int) strlen(p);
    int i = 0, j = 0;
    while (i < sLen && j < pLen) {
        if (j == -1 || s[i] == p[j]) {
            ++i, ++j;
        }
        else {
            j = next[j];
        }
    }

    if (j == pLen) {
        return i - j;
    }
    else {
        return -1;
    }
}
    原文作者:KMP算法
    原文地址: https://blog.csdn.net/tobebetterprogrammer/article/details/52781418
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞