KMP算法详解

KMP算法是解决字符串匹配的常用算法之一,也就是在主串(比如aabbccdd)中的子串(bc)定位问题。子串称为P,如果它在一个主串称为T中出现,就返回它的具体位置,我们先来看看普通的字符串匹配是怎么做的

最基础的匹配

思路:从左到右一个个匹配,如果这个过程中有某个字符不匹配,将子串向右移动一位,继续从左到右一一匹配。

当匹配到如图第四个字符位置后,匹配失败,子串后移,继续匹配


image

第一位匹配失败,继续后移...


image

image

直到匹配成功

[图片上传失败...(image-bdd392-1548667452278)]
代码如下:

public class Normal {
    
    public static void main(String[] args) {
        int index = bf("ABCABCEFG", "ABCE");
        System.out.println(index);
    }
    
    public static int bf(String ts, String ps) {
        char[] t = ts.toCharArray();
        char[] p = ps.toCharArray();
        int i = 0; // 主串的位置
        int j = 0; // 子串的位置

        while (i < t.length && j < p.length) {
            if (t[i] == p[j]) { // 当两个字符相同,就比较下一个
                i++;
                j++;
            } else {
                i = i - j + 1; // 一旦不匹配,i后退
                j = 0; // j归0
            }
        }

        if (j == p.length) {
            return i - j;
        } else {
            return -1;
        }
    }
}

这种方式是效率最低,匹配次数最多的情况,接下来看KMP的解决思路

KMP中的PMT

KMP在遇到下图位置时,不会很无脑的把子串的j移动到第0位,主串的i移动到第1位,然后进行T[i]==P[j]的比较
[图片上传失败...(image-1dbb87-1548667452278)]
因为从图上可以看得出后移一位后子串前三位(ABC)和主串的T[1-4](BCA)肯定是不匹配的,无需白白浪费这几次比较,最好应该是直接让i不变,j==0,如下图

image

从这里开始匹配,省去了前面的几次无用匹配。

KMP思想:利用前面匹配的信息,保持i指针不变,通过修改j指针,让子串尽量地移动到有效的位置。

整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪?

先用肉眼来看一下规律:

image

如图:C和D不匹配了,我们要把j移动到哪?显然是第1位。为什么?因为前面有一个A相同可以用:

image

再看一种:

image

可以把j指针移动到第2位,因为前面有两个字母是一样的:

image

我们可以看出来,匹配失败的时候,j变为k,j前面的的n个字符等于子串开头到k位置的n个字符的值

image

即:P[0 ~ k-1] == P[j-k ~ j-1]

这时我们发现规律了,其实就是要求当前j之前的字符串也就是ABCAB它的首尾对称的长度最大长度也就是PMT值。

PMT中的值是字符串的前缀集合与后缀集合的交集中最长元素的长度。

例如,对于”aba”,它的前缀集合为{”a”, ”ab”},后缀集合为{”ba”, ”a”}。
两个集合的交集为{”a”},
那么长度最长的元素就是字符串”a”了,长度为1,所以对于”aba”而言,它在PMT表中对应的值就是1。
再比如,对于字符串”ababa”,它的前缀集合为{”a”, ”ab”, ”aba”, ”abab”},
它的后缀集合为{”baba”, ”aba”, ”ba”, ”a”}, 
两个集合的交集为{”a”, ”aba”},其中最长的元素为”aba”,长度为3。

所以上面最后一个图的情况下,j位置之前的字符串的PMT值为2,所以j的值变成2。

KMP之next数组

那么好了接下来核心就是求得P串每个下标元素对应的k值即可,因为在P的每一个位置都可能发生不匹配,我们要计算每一个位置j对应的k,所以用一个数组next来保存,next[j] = k,表示当T[i] != P[j]时,j应该变为k。

求next数组代码如下

public class Next {
    
    public static int[] getNext(String ps) {
        char[] p = ps.toCharArray();
        int[] next = new int[p.length];
        next[0] = -1;
        int j = 0;
        int k = -1;
        while (j < p.length - 1) {
            if (k == -1 || p[j] == p[k]) {
                next[++j] = ++k;
            } else {
                k = next[k];
            }
        }
        return next;
    }
}

通过上面代码可以直接算出j为0和1时的k,当j为0时,已经无法后退了所以设置为-1初始化值,当j为1时,它的前面只有下标0了,所以next[0]=-1,next[1]=0.

接下来就是两种主要情况了

if (k == -1 || p[j] == p[k]) {   第一种p[j] == p[k]
    next[++j] = ++k;
} else {                         第二种p[j] != p[k]
    k = next[k];
}

第一种p[j] == p[k]

image

p[j] == p[k]时,有next[++j] = ++k;
因为当在p[j-1]处匹配失败后,j-1变为k-1,从k-1处重新开始匹配,原因就是他们共同有一个前缀A,所以当p[j] == p[k]后,他们就拥有了前缀AB所以k++;

第二种p[j] != p[k]

image

此时代码是:k = next[k];原因看下图

image

像上边的例子,我们已经不可能找到[ A,B,A,B ]这个最长的后缀串了,但我们还是可能找到[ A,B ]、[ B ]这样的前缀串的。所以这个过程就像在定位[ A,B,A,C ]这个串,当C和主串不一样了(也就是k位置不一样了),那当然是把指针移动到next[k]。

有了next数组之后就一切好办了,我们可以动手写KMP算法了:

public class Kmp {
    public static int KMP(String ts, String ps) {
        char[] t = ts.toCharArray();
        char[] p = ps.toCharArray();
        int i = 0; // 主串的位置
        int j = 0; // 模式串的位置
        int[] next = getNext(ps);

        while (i < t.length && j < p.length) {
            if (j == -1 || t[i] == p[j]) { // 当j为-1时,要移动的是i,当然j也要归0
                i++;
                j++;
            } else {
                // i不需要回溯了
                // i = i - j + 1;
                j = next[j]; // j回到指定位置
            }
        }

        if (j == p.length) {
            return i - j;
        } else {
            return -1;
        }
    }
}

KMP改进

KMP算法是存在缺陷的,来看一个例子:比如主串是aaaabcde,子串是aaaaax,next值为012345,当i=5时,如下图:

image

我们发现,当中的②③④⑤步骤,其实是多余的判断。由于子串的第二、三、四、五位置的字符都与首位的“a”相等,那么可以用首位next[1]的值去取代与它相等的字符后续next[j]的值,这是个很好的办法。因此我们对求next函数进行了改良。

public class Next2 {
    public static int[] getNext(String ps) {
        char[] p = ps.toCharArray();
        int[] next = new int[p.length];
        next[0] = -1;
        int j = 0;
        int k = -1;
        while (j < p.length - 1) {
            if (k == -1 || p[j] == p[k]) {
                if (p[++j] == p[++k]) { // 当两个字符相等时要跳过
                    next[j] = next[k];
                } else {
                    next[j] = k;
                }
            } else {
                k = next[k];
            }
        }
        return next;
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 字符串匹配KMP算法详解 1. 引言 以前看过很多次KMP算法,一直觉得很有用,但都没有搞明白,一方面是网上很少有...
    张晨辉Allen阅读 2,382评论 0 3
  • 概述 KMP是字符串匹配的经典算法。其中包含的思想,是非常有趣的。本文作为KMP算法的介绍和备忘录。 场景 KMP...
    oceanLong阅读 1,616评论 1 1
  • 在数据结构课上老师讲了kmp算法,但当时并没太懂,现在把思路重新理一遍。 1.kmp算法简介 KMP是三位大牛:D...
    zealscott阅读 248评论 0 1
  • 原链接:KMP算法详解|CloudWong 传统的字符串匹配模式(暴力循环) 子串的定位操作通常称作串的串的匹配模...
    简Cloud阅读 3,896评论 1 22
  • 详解:https://www.cnblogs.com/yjiyjige/p/3263858.htmlhttps:/...
    小幸运Q阅读 376评论 0 1