真的有这么丝滑吗?近日国外一小哥深入研究了KMP算法……

近日被朋友问到了字符串匹配算法,让我想起了大二上学期在一次校级编程竞赛中我碰到同样的问题时,为自己写出了暴力匹配算法而沾沾自喜的经历。

现在想来,着实有点羞愧,于是埋头去学习了一下KMP算法,为了让自己不至于那么快忘记,也希望小伙伴们能从我的理解中收获一点自己的感悟!

文章伴有精心雕琢的动画以便理解。

我们首先来分析一下暴力算法,为鲜花的诞生献上绿叶!

以下文中统一将需要被匹配的字符串(长的那段)称为待匹配串 ,把用来匹配的字符串(短的那段)称为模式串

暴力匹配算法的思路很简单,就是每一次都首先将待匹配串和模式串的首字母对齐,然后比对是否相同,若相同则继续比对两个串的下一个位置,如果不相同的话就将模式串向右移动一位,然后再重新开始从头匹配,就像下面这样⬇️⬇️

暴力匹配演示

从上面的动画我们可以直观的看出来,下面的模式串在匹配失败之后都只会移动一格,傻里傻气的,这就导致它的时间复杂度是M*N,其中M是模式串的长度,N是待匹配串的长度。

对于这个时间复杂度,我不满意!它太傻了,不符合我聪明睿智的气质!

那就来分析一下为何它这么傻。我们可以看到,在第一次匹配失败的时候,我们肯定希望它向右移动至少两格,因为模式串的第一格和第三格都为a,既然第三格已经匹配成功了,那么把第一格对上第三格匹配的位置,那么无疑肯定也是可以成功的,我们的算法本该知道并且利用这一点的!但是它没有,它太傻了。

嗯,这么一说,好像是感觉应该是要把它向着动态规划的方向改(即利用已有信息为下一步提供便利)。

PS:字符串问题百分之八十以上都可以使用动态规划思想达到较低的时间复杂度。

我们大都听过一句老话:人啊,贵在有自知之明。

同时我们肯定也听别人说过:人只有深刻的认识了自己,才能找对位置,迅速地向目标前进!

这两句话用在KMP算法中再合适不过了!

KMP算法的核心便在于,模式串对自己的自我认知!

想一想,我们人对自己的认知是如何的:男,19岁,阳光帅气聪明机智,这些自我认知都存放在我的脑袋里面。

那么,模式串对自己的认知应该存放在哪呢?

对,就是next数组里面!字符串没有大脑,所以它需要额外的空间来存储它对自己的认知并籍此作出高效准确的判断。

那么字符串对自己的认知是怎样的呢?其实很容易理解,就是知道自己身上哪些地方是相同的,这样的话在匹配失败之后就能迅速找准下次开始的点。这里是不是有点模糊了?图来!


KMP算法拟人化演示

以上就是KMP算法的动画,如果觉得动画稍微有点快的话可以多观看几次,在这个动画里我还没有放出next数组的部分,只是用拟人化的手法展现出来。希望大家能够理解,为什么第一次匹配失败可以直接移动两格。

是因为模式串中第三格的a,它知道在第一格有与自己相同的字符,并且把这个信息告诉下一格的字符,让它在匹配失败之后直接把第一格的a移动到它的那个位置上去。

我这里为了大家容易理解,只放出了一个字符相同的情况,大家不妨可以扩展想一下,假如,第一格和第三格的a不是一个字符,而是一个字符串呢?怎么?有点打脑壳?图来!


KMP算法动画

来看看模式串与其对应的next自我认识数组吧。

i 0 1 2 3 4 5 6
next -1 0 0 0 1 2 3
string a b c a b c d

不要去在意next数组的第一个为什么是-1,这是为了代码写的方便,暂且就给它当成0.

在动画中,当一个字符发出“直接移动”的语句的时候,其实是告诉后一个字符,如果你匹配失败了的话,就直接移动,同时后一个字符对应的next数组值为0,当后一个字符匹配失败了,就移动模式串的长度-这个匹配失败的字符对应的next值个长度。

从第四个字符(i=3)起,它们都在不断告诉后面一个字符:“将i=0移动到i=3的位置”,这句话对于i=4的字符来说,是移动4-1格, 对于i=5的字符来说,是移动5-2格,对于i=6的字符来说,是移动6-3格:后面那个减数恰好就是这个字符对应的next数组的值!

因为模式串足够了解自己,所以它能够在匹配失败的时候不用回退,不用每次只移动一格,而是跟随着待匹配串一起移动。待匹配字符串的指针从未回退过,以线性的速度向前一步步越进。

最终:KMP算法的时间复杂度是M+N

这里我们不禁发出了感叹!原来认识自己真的这么重要啊!

接下来是求出给定模式串的next数组:

python3代码奉上⬇️⬇️

def get_next_lst(ss: str) -> list:
    length = len(ss)
    next_lst = [0 for _ in range(length)]
    next_lst[0] = -1
    i = 0
    j = -1
    while i < length - 1:
        if j == -1 or ss[i] == ss[j]:
            i += 1
            j += 1
            next_lst[i] = j
        else:
            j = next_lst[j]
    return next_lst

这段代码最难理解的就是j=next_lst[j]这句话,其实这句话也是动态规划的一个思想,看我为你剖析一下。

在这里插入图片描述

已知蓝色区域相等且长度都为len,那么很明显,next[i] == len,若此时模式串pattern[i] != pattern[j](两个灰色区域不相等)。那么看下图:

在这里插入图片描述

若此时next[j] == len(粉色部分)那么S1==S2,又因为next[i] == next[j],所以S1==S3 且 S3 == S4,则可以推出S1 == S4,这样我们就利用前面所获得的信息,推出了S1 == S4这个信息,然后将J移动到S1后一格,只要再次比较patter[i] 与 patter[j]的相等情况,就可以得出next[i+1]的值。这里因为i始终向后移动,所以也是线性时间复杂度的算法。

ohhhhhhhhh~

到这里,大家就明白了为啥KMP算法的时间复杂度是M+N了。

KMP匹配字符串的完整代码附上!

class KMP():
    def __init__(self, ss: str) -> list:
        self.length = len(ss)
        self.next_lst = [0 for _ in range(self.length)]
        self.next_lst[0] = -1
        i = 0
        j = -1
        while i < self.length - 1:
            if j == -1 or ss[i] == ss[j]:
                i += 1
                j += 1
                self.next_lst[i] = j
            else:
                j = self.next_lst[j]
        self.pattern = ss
    
    def match_str(self, ss:str):
        ans_lst = []
        j = 0
        for i in range(len(ss)):
            if ss[i] != self.pattern[j]:
                j = self.next_lst[j] if self.next_lst[j] != -1 else 0
            if ss[i] == self.pattern[j]:
                j += 1
            if j == self.length:
                return i + 1 - self.length
        return -1


tmp_kmp = KMP('iabc')
print(tmp_kmp.match_str('adosjfoiajsoifjasiofjoiasdjoiabc'))

看到这里,如果你觉得这篇文章对你理解KMP算法有帮助的话呢,不妨关注我,我会持续更新各种有用的东西。我的个人公众号是【程序小员】,也欢迎你的关注哦!

我是落阳,谢谢你的到访~

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