KMP算法-劝退刷题的两大门神之一,总算搞懂了

KMP算法

假设字符串str长度为N,字符串match长度为M,M <= N,想确定str中是否有某个子串是等于match的。

时间复杂度O(N)

暴力解

直接遍历,以i(0~N-1)位置开始尝试趟一遍match看看是否能够匹配,如果不匹配就i++继续尝试,如果有一个i是能够匹配的就返回这个i,否则返回-1。最坏情况下,每次都趟了M-1个字符,在最后一个才发现不匹配,然后直到i==N-M的时候才匹配成功。这样时间复杂度就是O(N*M),如果M非常接近N,那么时间复杂度接近O(N^2)。

KMP算法就是将这个匹配过程给优化到O(N),着眼点就是暴力匹配中存在大量的重复比较。

必要铺垫

在介绍KMP算法前先介绍一个概念

字符串i位置字符前的字符串中,前缀字符串和后缀字符串相等的最大长度(<i前字符串长度),这两个字符串的长度都是要小于i位置前的字符串长度的。

image-20210312231809950.png

这个某个位置前的字符串最长相同前后缀字符串长度(这玩意我也不知道学名是啥)就是KMP算法的精髓,0位置的这个值人为规定为-1,因为前面没有字符,1位置这个值人为规定为0,因为前面就一个字符,要求长度要小于前面的字符串长度,所以小于1,设置为0无可厚非。

假设我们已经获得了每个位置的这个长度的值,那么怎么能够优化比较的过程呢?

这就是KMP算法真正优化的地方了,暴力匹配的问题就是i位置开始如果匹配不成功下一次就从i+1位置开始进行重新匹配,从而浪费了之前比较失败的一些字符串特征。

KMP算法流程

之后为了简单起见那个最长...长度我就直接叫maxPrePosEqlLen

image-20210313091708975.png

理解流程的两个关键点

1.为什么str指针不用动,留在x,而match指针要回退到j位置?

image-20210313092742955.png

由于str是从i位置开始和match进行匹配,直到x,y位置才匹配不上的,所以str中[i...x-1]和match中[0...Y-1]部分是完全相同的,而match中[0...j-1]部分正好是Y位置的最长前缀字符串,str中[x-j...x-1]部分正好等于Y位置的最长后缀字符串,所以它们肯定相等就不用比对了,让str直接从x开始,match直接从j开始继续比对。

2.像是暴力解很好理解,i位置比对不上,那我从i+1位置继续尝试比对就好了,那肯定是正确的。为什么我现在不从i+1位置开始比对,而是从x-j位置比对呢?这样能保证正确性吗?

证明:为什么从i+1到x-j-1位置开始没有一个是能够匹配match的?

如果这些位置都不可能,那么肯定可以直接淘汰,我从x-j位置开始尝试匹配就合理了,而且大大加速了匹配的速度。

image-20210313094058783.png

反证法

假设从i+1到x-j-1中间的某个k位置出发能够匹配match,那么就会有str中k...x-1这一段匹配上match中0...x-k-1这段,由于str中i...x-1和match中0...Y-1这一段完全相等,而k...x-1这一段距离x-k是肯定比j要长的,所以说Y位置就会有一个比j还要长的maxPrePosEqlLen。这是不可能的,因为我们一开始求的maxPrePosEqlLen就是最长的,就是建立在求的maxPrePosEqlLen正确的情况下进行算法的。所以假设不成立,i+1到x-j-1中间任意某个k位置出发必不可能匹配match。所以可以从x-j重新出发。

KMP算法证明成立,可以跳过i+1到x-j-1中间的所有位置,从x-j位置重新出发,这真的起到了加速效果吗,时间复杂度能否收敛到O(N)?

算法实现和分析时间复杂度

算法实现(java)

public static int getIndexOf(String s, String m) {
    if (m.length() > s.length()) return -1;
    char[] str = s.toCharArray();
    char[] match = m.toCharArray();
    //maxPrePosEqlLen信息就存在这个数组中i位置的值就对应nexts下标i的值
    int[] nexts = getNextArray(match);
    int x = 0, y = 0;//x是str的工作指针,y是match的
    //为什么循环条件这么写,因为x,y都是从0位置往后匹配的,匹配不上则y回跳
    //匹配上则x前走,不管匹配得上还是匹配不上,最终总有至少有一个会到length
    while (x < str.length && y < match.length) {
        if(str[x]==match[y]){
            x++;y++;//匹配上了,都后移,非常合理
        }else if(nexts[y]>=0){
            //x不动,y移动到最长前缀的后一个位置,假如最长前缀长度为j,那么0~j-1就是最长前缀,所以j就是现在y应该去的地方,这也是为什么maxPrePosEqlLen数组叫做next数组,代表了y下一跳去的地方
            y = nexts[y];
        }else{
            //说明str指针从x-j位置到x位置开始一个都匹配不上,那么x以及x前的位置淘汰,x++,很合理
            x++;
        }
    }
    /*
    分析:
    1.x==str.length&&y==match.length,那么说明x从str.length-match.length位置开始匹配上了match
    2.x==str.length&&y!=match.length,那么说明str都走完了,所有位置都淘汰了还是匹配不上match,返回-1
    3.x!=str.length&&y==match.length,说明str中间某个串就匹配上了match,这个开始位置是x-match.length
     */
    return y==match.length?x-match.length:-1;//结合所有情况,合理
}

时间复杂度

两种求解方式

方式一

image-20210313103500286.png

每个分支其实逻辑上都会造成match数组向右移和str对齐,而match最多向右走N-M,所以是O(N)

方式二

image-20210313104620442.png

这个分析方式就很骚了,虽然你y是左右横跳,但是x-y的值却是最大为N的,而且处于单调上升的趋势,所以不管你y啥取值,最终也是只会蹦跶N次直到x-y到最大值N就停止。而x最大也是取到N,所以进入循环最多2N次,整体时间复杂度O(N)。

OK,KMP算法整体流程咱们分析过了,只要现在求解next数组的时间复杂度收敛到O(N),那么算法就能够收敛到O(N)了。

求解next数组

next数组也就是maxPrePosEqlLen数组了,因为可以代表y下一跳的位置,所以叫next数组。

分析:match数组长度为M

①如果y==0,那么next[y]==-1,人为规定,没得好说

②如果y==1,那么next[y] ==0,人为规定,没得好说

③y>=2怎么求?

image-20210313125540509.png

算法实现和分析时间复杂度

public static int[] getNextArray(char[] match) {
    if (match.length == 1) return new int[]{-1};
    int[] nexts = new int[match.length];
    nexts[0] = -1;
    nexts[1] = 0;
    int cur = 2;
    //这代表的是cur前一个位置的next的值也代表需要和cur比对的match中的下标,理解这个很重要
    int preNext = 0;
    while (cur < match.length) {
        if (match[cur - 1] == match[preNext]) {
            //每个cur位置都是要看上一个位置的next的,上个位置设置的时候next = preNext+1,所以就干脆直接修改preNext为preNext+1了
            nexts[cur++] = ++preNext;
        } else if (preNext>0) {//nexts[preNext]>=0同于preNext>0
            //不符合还能往前跳就继续往前跳
            preNext = nexts[preNext];
        } else {
            nexts[cur++] = 0;
        }
    }
    return nexts;
}

时间复杂度

和之前一样用骚操作来推算,preNext虽然反复横跳,但是cur-preNext是要么不变,要么递增的。

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

推荐阅读更多精彩内容

  • 今天看了kmp算法,最开始看得特别混乱,最后终于看明白了,想记录一下。https://github.com/hym...
    不会code的程序猿阅读 965评论 0 4
  • 标签(空格分隔): 数据结构与算法 KMP算法是一个经典的字符串匹配算法,但是原理比较晦涩难懂,这里推荐一篇个人感...
    Sivin阅读 966评论 0 2
  • 现在写文章,也是痛点在哪,就写哪?今天的痛点是老是记不住KMP算法。我曾经3次拿下KMP算法。但令人遗憾的是,我又...
    西部小笼包阅读 1,037评论 0 2
  • KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此...
    Simplelove_f033阅读 380评论 0 0
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,548评论 0 11