通俗易懂的KMP算法详解

一:什么是KMP算法?

KMP诞生背景:

KMP(Knuth-Morris-Pratt)三位大佬联名提出,故以他们姓名的首字母命名,不得不说,他们的贡献巨大,因为在计算机的世界,子串模式匹配的场景非常多,越是底层的地方,其运行的性能越是重要。在KMP算法问世之前,其实在子串匹配上采用的都是暴力匹配,我们一般称之为朴素算法,可能是因为算法比较容易想到吧,所以很朴素。

朴素匹配算法:

假设称主串为str,模式串为ptr,如果采用暴力匹配,那其实就是将str和ptr逐个字符进行比较,如果匹配失败了,就将str后移一个位置,然后在进行逐一比较,考虑一下最差情况的时间复杂度达到了(str.length-ptr.length)*ptr.length。显然这是一种较差的算法。


朴素匹配算法.png

下面给出一下实现代码吧,就当为KMP算法预预热:

public class NaiveMatchingString {
    /**
     * 字符串匹配算法,简单易理解的就是暴力匹配,但是时间复杂度较高
     */
    /**
     *
     * @param str:主串
     * @param ptr:用于匹配的模式串
     * @return  返回的是ptr匹配上str的时候,str的第一个字符所在字符串的位置,如果匹配不上
     * 则返回0
     */
    public int NaiveMatch(String str,String ptr){
        int str_index = 0;// 表示匹配str所在的脚标
        int str_move_index; // 表示匹配ptr过程中移动的脚标
        int ptr_index;// 表示匹配ptr所在的脚标
        for (str_move_index = str_index;str_index<=str.length()-ptr.length();str_index++,str_move_index++){
            for (ptr_index = 0;str.charAt(str_move_index) == ptr.charAt(ptr_index);ptr_index++,str_move_index++){
                if (ptr_index == ptr.length()-1){return str_index;}
            }
            str_move_index = str_index;
        }
        return 0;
    }

    public static void main(String[] args) {
        String str = "dabxabxababxabwabxad";
        String ptr = "abxabwabxad";
        NaiveMatchingString naiveMatchingString = new NaiveMatchingString();
        System.out.println(naiveMatchingString.NaiveMatch(str,ptr)); // 9
    }
}

二:KMP算法核心解决了什么问题?

子串匹配.png

回想一下朴素匹配算法,当ptr和str进行逐一比较时,一旦中间哪一个不匹配了,str只是往后移动了一个位置,KMP算法核心就是根据ptr(模式串)匹配中str的部分求得最大的前后缀,然后根据这个可以获得str(主串)往后最大可以移动几个位置。从而可以节约时间复杂度。

举个简单的例子:


kmp1.png

如上图,第一次匹配str(1)=d,ptr(1)=a,所以不匹配,即str++,第二次匹配一直匹配到str(7)!=ptr(6),按照朴素匹配的方式,继续将str++,但是仔细观察一下ptr匹配中str的部分,即abxab,如果只是简单的str++,那么第三次开始比较时ptr(2)=b,明显不会等于a。既然我们已经从之前匹配中的字符串当中可以看出一些信息,那么就不需要那么死板的暴力匹配了,那么该如何移动str的位置呢?回到abxab这个字符串当中,其前缀包含(a,ab,abx,abxa),不包含最后一个字符,其后缀为(b,ab,xab,bxab)不包含第一个字符,最大的公共前后缀是ab,这说明了啥,说明了可以这样子移动,见下图:


kmp2.png

从这里就可以了解到,KMP算法主要是解决以下问题:
1:通过观察之前匹配中的字符串,求出最大公共前后缀,使得str往后移动不是渐增,避免不必要的比较操作。

三:next数组怎么求?

上述只是针对匹配中的字符串abxab的一种解释,可以想象一下,在ptr和str匹配的过程当中,匹配中的字符串存在的可能性有多少种,是不是ptr含有多少元素就有多少种,而且每种都存在唯一的最大公共前后缀,其实求解这个的过程,就是求解著名的next数组,能够把这个理解了,KMP算法也就理解了八八九九了,next数组里面存放的是最大公共前后缀的长度,其决定了str每次往后移动的位置个数。

next数组.png

接下来对这副图进行一定的解释:

  • i值表示匹配中ptr字符串的下标,比如只匹配中a,下标为0。
  • 最右边的next[i]值,其实就是我们最终要求得的next数组,每个结点存放的是k值,k值可以理解为匹配中字符串的最大公共子串,可以看到当i=0,1,2时,最大的公共前后缀都是0,因为没有。
  • 解释一下k,i指针,i指针很简单,每次都指向字符串的最后,k指针初始下标为0,然后与i下标的值进行比较,如果相等,则k++,不相等了,就将当前k的值存入next数组就好。
  • 进行比较时,k下标和i下标不相等,其实分为两种情况,第一种情况是第一次比较时相等的,当k++以后(可能存在多次),在比较可能存在不相等,这种情况直接将当前的K值存入next数组就好,还有一种情况,就是第一次比较都不相等,这种情况需要将k = next[k-1],为什么这么操作,稍后解释。获取新的k值以后,再次进行与i比较,跟上述流程一样,这里需要注意的是,当k=0了,并且第一次比较就不相等,那么next[i]直接等于0了。
  • 解释一下为什么当k!=0时,第一次比较就不相等要让k=next[k-1],观察下图:


    kmp3.png

    假设当i=9时,这时k=4,最大公共前后缀是abxa,这个abxa的最大最后缀是a,并且k指针现在指向b。然后当i=10时,i指针指向d,由于k指针指向b,b!=d,这时候是会触发我们刚才说的表达式k=next[k-1],这样子,k=1,即指向b,可能这时候就有疑问为什么不指向a,要指向b,仔细想想,当i=9时,k指针指向b,b不等于i指针指向的a,所以导致其最大公共前后缀只有4长度,如果相等了,那么长度就是5了。而且abxa的最大公共前后缀是a,那么就可以说明k指针指向的位置b肯定也不会等于i=10时的第一个字符a,因为它是abxa的最大公共前后缀。所以求k的表达式是k=next[k-1],这是有讲究的。

求解next数组代码:

/**
     * 获取next数组,KMP算法的核心。
     * 主要步骤如下:
     * 1、next数组的大小为ptr.length
     * 2、每次计算最大前后缀的范围为0-next的下标
     * 3、创建两个指针,一个为k指针,一个为i指针,用比较对应下标位置的字符是否一致
     * 4、如果ptr.CharAt(k) == ptr.CharAt(i),则k++,如果非第一次比较不相等,则k等于++后的值
     * 5、如果第一次比较就不相等则k = next[k-1],然后继续执行与i下标的比较
     * 6、一直循环至next[ptr.lenth-1],next数组存放的是k的值
     *
     * @param ptr
     * @return
     */
    public int[] getNext(String ptr) {
        int K = 0; //初始化K指针
        int I; // I指针始终指向最后的位置
        int nextIndex = 1; //表示计算那个next脚标的数据,因为next[0] = 0,从1开始计算
        int[] next = new int[ptr.length()];
        next[0] = 0;
        for (; nextIndex < ptr.length(); ) {
            String substring = ptr.substring(0, nextIndex + 1);
            I = substring.length() - 1;
            if (substring.charAt(K) == substring.charAt(I)) {
                while (K < substring.length() / 2 && substring.charAt(K) == substring.charAt(I)) {
                    K++;
                }
                next[nextIndex++] = K;
            } else {
                if (K == 0){
                    next[nextIndex++] = 0;
                }else{
                    K = next[K - 1];
                    continue;
                }
            }
        }
        return next;
    }

四:KMP算法实现

public class KMPStringMatching {
    /**
     * KMP算法:一种效率匹配子串的算法
     */

    /**
     * 获取next数组,KMP算法的核心。
     * 主要步骤如下:
     * 1、next数组的大小为ptr.length
     * 2、每次计算最大前后缀的范围为0-next的下标
     * 3、创建两个指针,一个为k指针,一个为i指针,用比较对应下标位置的字符是否一致
     * 4、如果ptr.CharAt(k) == ptr.CharAt(i),则k++,如果非第一次比较不相等,则k等于++后的值
     * 5、如果第一次比较就不相等则k = next[k-1],然后继续执行与i下标的比较
     * 6、一直循环至next[ptr.lenth-1],next数组存放的是k的值
     *
     * @param ptr
     * @return
     */
    public int[] getNext(String ptr) {
        int K = 0; //初始化K指针
        int I; // I指针始终指向最后的位置
        int nextIndex = 1; //表示计算那个next脚标的数据,因为next[0] = 0,从1开始计算
        int[] next = new int[ptr.length()];
        next[0] = 0;
        for (; nextIndex < ptr.length(); ) {
            String substring = ptr.substring(0, nextIndex + 1);
            I = substring.length() - 1;
            if (substring.charAt(K) == substring.charAt(I)) {
                while (K < substring.length() / 2 && substring.charAt(K) == substring.charAt(I)) {
                    K++;
                }
                next[nextIndex++] = K;
            } else {
                if (K == 0){
                    next[nextIndex++] = 0;
                }else{
                    K = next[K - 1];
                    continue;
                }
            }
        }
        return next;
    }

    /**
     *
     * @param str:主串
     * @param ptr:模式串
     * @return
     */
    public int getStrIndex(String str,String ptr){
        //先获取模式串的next数组
        int[] next = getNext(ptr);
        int str_index = 0; //表示匹配主串的位置
        int str_move_index; //表示匹配主串中移动的位置
        int ptr_index; //表示匹配模式串中的位置
        for (;str_index<=str.length()-ptr.length();){
            for (ptr_index = 0,str_move_index = str_index;ptr.charAt(ptr_index) == str.charAt(str_move_index);ptr_index++,str_move_index++){
                if (ptr_index == ptr.length()-1){
                    return str_index;
                }
            }
            //获取新的str_index值
            int ne;
            if (ptr_index == 0){
                ne = 0;
                str_index++;
            }else{
                ne = next[ptr_index -1];
                if (ne == 0){
                    str_index++;
                }else{
                    str_index = str_index + (ptr_index - ne);
                }
            }
        }
        return 0;
    }

    public static void main(String[] args) {
        String str = "dabxabxababxabwabxad";
        String ptr = "abxabwabxad";
        KMPStringMatching kmpStringMatching = new KMPStringMatching();
        int strIndex = kmpStringMatching.getStrIndex(str, ptr);
        System.out.println(strIndex); //9
    }
}

kmp4.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350