美文网首页
KMP算法理解

KMP算法理解

作者: 圣光忏悔 | 来源:发表于2017-02-23 11:34 被阅读673次

KMP的由来

在KMP算法之前,对文本进行匹配时使用的是朴素模式匹配算法,也就是最简单匹配算法.
当然运行效率也是让人深恶痛绝,举个例子:

现有长度为n的模式串00001,和长度为m的文本串0000000000000000000000000000000001.
按照朴素匹配算法最终时间复杂度为o{(m-n+1)*n}

这种有多个0和1重复字符的字符串,匹配模式却仍需要挨个遍历,这是十分糟糕的,所以最终KMP算法诞生了.

KMP和朴素算法的核心差别

朴素算法在匹配时,子串和对比的目标串都会不断的进行回溯对比,而KMP会先计算出子串的匹配数组,在进行匹配时目标串并不会进行回溯,且子串回溯时会根据计算出的重复数组省略很多不必要的回溯.
下面就用例子进行讲解.

KMP的回溯原理

引用阮一峰:字符串匹配的KMP算法

以下图两字符串匹配为例:

未编译

KMP核心是找出要匹配的子串的匹配值数组,如下图给出的子串和匹配值.

未编译

"子串匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,
"A"的前缀和后缀都为空集,共有元素的长度为0;
"AB"的前缀为[A],后缀为[B],共有元素的长度为0;
"ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
"ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
"ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
"ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
"ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

跳过前面一端不匹配的,我们直接来到匹配串生效的位置

已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为 6 - 2 等于4,所以将搜索词向后移动4位。

因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。

因为空格与A不匹配,继续后移一位。

逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。

这么一看KMP是不是比朴素算法匹配次数少了很多?例子中也看出了KMP核心在于匹配数组,那如何通过代码求出匹配数组呢?

KMP核心匹配串数组

下面是Java的伪代码


    public int[] get_next(char[] str) {
        int i = 1;
        int j = 0;
        int[] next = new int[str.length];
        next[0] = 0;
        while (i < str.length) {
            if (str[i]==(str[j])) { // 匹配记录下当前匹配的位置
                ++j;
                next[i] = j;  // 为了配合例子更便于理解,所以在++i之前便给匹配数组进行了赋值
                ++i;
            } else if (j == 0) {  // 完全不匹配,跳至下一个字符
                next[i] = j;  // 同上理由
                ++j;
                ++i;
            } else {
                j = next[j - 1];  // 回溯至合适的位置
            }
        }
        return next;
    }

这段代码中j = next[j - 1];是很多人懵逼的地方,当然常规模式是j = next[j];.我这里配合例子进行了改写.
为什么不是递减回溯,而是直接跳至数组中的匹配值,尽管从结果中可以看出这样确实是高效的方式,但是为什么正好就是最合适呢?知其然不知其所以然.

下面按照上面思路来实现KMP算法就能知其所以然了.

KMP算法具体实现

    // 求字符串t,在字符串s中的pos位之后首次出现的位置
    public int index_KMP(char[] s, char[] t, int pos) {
        int i = pos;
        int j = 0;
        int[] next = get_next(t);
        while (i < s.length && j < t.length) {
            if (s[i] == t[j]) {
                i++;
                j++;
            } else if (j == 0) {
                i++;
            } else {
                // j的回溯位置可以根据上面例子中的公式来套.
                // 移动位数 = 已匹配的字符数 - 对应的部分匹配值;
                // 已匹配的字符数为: j;
                // 对应部分匹配值为: next[j - 1];
                // 移动位数为: j - (next[j - 1]);
                // 新的索引 = 旧索引 - 移动位数;
                // j = j - (j - (next[j - 1]));
                j = next[j - 1];
            }
        }
        if (j == t.length) {
            return i - t.length;
        }
        return 0;
    }

从这里就可以看出为什么回溯的位置是j = next[j - 1],在于常规的对应一下,常规的kmp算法数组中当前位对应的是前一位的重复值,并不是本身的重复值.
在上述公式中我们需要找到前一位的对应值,来进行位移操作,而常规KMP中前一位重复值就是next[j].
所以最终的回缩过程就是 j = next[j];

便于理解,在求自身匹配数组时,也可以将不断变换的尾串看做成子串,这样就是子串和目标串进行匹配,就可以套用上面的公式得出j = next[j],只不过尾串一直在变化而已.

在改进KMP算法时,需先将算法恢复到正常模式(想了半天不知道在改进模式的情况下如何继续推导了...)
常规的KMP匹配数组获取代码如下:

    // 由于数组的角标是从0开始的,所以的出来的结果和书上所说的统统小一.
    public int[] get_next(char[] str) {
        int i = 0;
        int j = -1;
        int[] nextval = new int[str.length];
        nextval[0] = -1;
        while (i < str.length - 1) {
            if (j == -1 || str[i] == (str[j])) {   // 匹配记录下当前匹配的位置
                ++j;
                ++i;
                nextval[i] = j;
            } else {
                j = nextval[j];  // 回溯至合适的位置
            }
        }
        return nextval;
    }

常规模式KMP匹配代码如下:

    public int index_KMP(char[] s, char[] t, int pos) {
        int i = pos;
        int j = 0;
        int[] next = get_next(t);
        while (i < s.length && j < t.length) {
            if (j == 0 || s[i] == t[j]) {
                i++;
                j++;
            } else {
                j = next[j];
            }
        }
        if (j == t.length) {
            return i - t.length;
        }
        return 0;
    }

KMP算法的改进

引自July:从头到尾彻底理解KMP(2014年8月22日版)
用前面的next 数组方法求模式串“abab”的next 数组,可得其next 数组为-1 0 0 1(0 0 1 2整体右移一位,初值赋为-1),当它跟下图中的文本串去匹配的时候,发现b跟c失配,于是模式串右移j - next[j] = 3 - 1 =2位。

P表子串(abab),s表目标串(abacabababc)
右移2位后,b又跟c失配。事实上,因为在上一步的匹配中,已经得知p[3] = b,与s[3] = c失配,而右移两位之后,让p[next[3]] = p[1] = b 再跟s[3]匹配时,必然失配。问题出在哪呢?

问题出在不该出现p[j] = p[next[j]]。为什么呢?理由是:当p[j] != s[i] 时,下次匹配必然是p[next[j]] 跟s[i]匹配,如果p[j] = p[next[j]],必然导致后一步匹配失败(因为p[j]已经跟s[i]失配,然后你还用跟p[j]等同的值p[next[j]]去跟s[i]匹配,很显然,必然失配),所以不能允许p[j] = p[next[j]]。如果出现了p[j] = p[next[j] ]咋办呢?如果出现了,则需要再次递归,即令next[j] = next[next[j]]。

    public int[] get_nextval(char[] str) {
        int i = 0;
        int j = -1;
        int[] nextval = new int[str.length];
        nextval[0] = -1;
        while (i < str.length - 1) {
            if (j == -1 || str[i] == (str[j])) {   // 匹配记录下当前匹配的位置;
                ++j;
                ++i;
                nextval[i] = j;
                if (str[i] != str[j]) { // 当前字符与前缀字符不同;
                    nextval[i] = j;  // 当前的j赋值给nextval[i];
                } else {
                    nextval[i] = nextval[j]; // 与前缀字符相同,那么将前缀字符的nextval值付给在i位的值.
                }
            } else {
                j = nextval[j];  // 回溯至合适的位置
            }
        }
        return nextval;
    }

本文到此结束.
前面KMP推导算是个人理解,后面的改进照搬July的,因为实在说的很清楚了,没有能补充的,若还是有点蒙圈,可以看July原文,或许更容易理解.

参考博客

阮一峰:字符串匹配的KMP算法
July:从头到尾彻底理解KMP(2014年8月22日版)

相关文章

  • 对KMP算法的一些理解

    最近学到KMP算法,下面讲讲对KMP算法的一些个人理解,希望对大家有帮助! 对于KMP算法的理解: 整个KMP算法...

  • KMP算法学习札记

    参考文章 知乎:如何更好的理解和掌握 KMP 算法?从头到尾彻底理解KMPKMP 算法(1):如何理解 KMP(原...

  • KMP算法 理解与实现

    KMP算法,背景不必多说,主要想写一写自己对KMP算法的一些理解和其具体实现。关于KMP算法的原理,阮一峰老师的这...

  • 深入理解KMP算法

    深入理解KMP算法 时间:20180313 KMP算法的核心是 求公共最大前后缀。 画图分析 继续分析如何利用前后...

  • KMP算法

    参考文献1.B站灯笼哥2.KMP算法python实现3.如何更好的理解和掌握 KMP 算法?

  • 数据结构与算法14-字符串匹配与KMP

    什么是KMP KMP算法是在字符串匹配算法中比较绕的.主要是需要理解KMP中next数组求解的必要性以及j 的回溯...

  • 字符串匹配问题-KMP算法

    什么是KMP KMP算法是在字符串匹配算法中比较绕的.主要是需要理解KMP中next数组求解的必要性以及j 的回溯...

  • KMP 专题整理

    KMP 学习记录 kuangbin专题十六——KMP KMP 学习总结 朴素 KMP 算法 拓展 KMP 算法(E...

  • 理解 KMP 算法

    1. 概述 字符串是编程中常用的一种数据结构,在各个方面都有广泛的应用,而字符串的一种基本操作就是给定一段长度为N...

  • 理解KMP算法

    简单理解KMP 本文读者可以获得两方面的知识 直观理解如何生成部分匹配表(下文简称匹配表),这是KMP算法的核心思...

网友评论

      本文标题:KMP算法理解

      本文链接:https://www.haomeiwen.com/subject/ekamwttx.html