Go 数据结构和算法篇(十二):字符串匹配之 KMP 算法

昨天

以下文章来源于xueyuanjun ,作者xueyuanjun

xueyuanjun学院君的订阅号,我会在这里持续更新优质全栈编程技术教程,包括但不限于 Golang、PHP、JavaScript 以及计算机底层技术。关注我,学习更多编程知识!

KMP 算法可以说是字符串匹配算法中最知名的算法了,KMP 算法是根据三位作者(D.E.Knuth,J.H.Morris 和 V.R.Pratt)的名字来命名的,算法的全称是 Knuth Morris Pratt 算法,简称为 KMP 算法。

一、核心思想

假设主串是 a,模式串是 b。在模式串与主串匹配的过程中,当遇到不可匹配的字符的时候,我们希望找到一些规律,可以将模式串往后多滑动几位,跳过那些肯定不会匹配的情况,从而避免 BF 算法这种暴力匹配,提高算法性能。下面我们来探讨下这个规律如何找到。

参考下面个主串和模式串的匹配,当模式串移动到当前位置,比对到最后一个字符 D 时,发现与主串不匹配,如果按照 BF 算法,就是把模式串往后移一位,再逐个比较,这样做固然可以,但是效率很差:

字符串匹配算法

一个基本事实是,当 D 与主串不匹配时,我们已知前面的主串序列是ABCDA,如果把模式串往后移一位肯定和主串不匹配,我们可不可以直接把模式串移到下一个可能和 A 匹配的主串位置?

实际上,KMP 算法正是基于这一理念,设法利用这个已知信息,不把模式串移到已经比较过的位置,继续把它向后移,这样综合下来就极大提高了搜索匹配效率。

怎么找到这个规律,确定把模式串往后移多少位呢?在模式串和主串匹配的过程中,我们把不能匹配的那个字符仍然叫作「坏字符」,把已经匹配的那段字符串叫作「好前缀」:

KMP匹配算法图示

在模式串和主串匹配的过程中,当遇到坏字符后,对于已经比对过的好前缀,我们只需要拿好前缀本身,在它的后缀子串中,查找最长的那个可以跟好前缀的前缀子串匹配的下标位置,然后将模式串后移到该位置即可。

这里,我们要解释几个概念:

  • 后缀子串:以某个字符串最后一个字符为尾字符的子串(不包含字符串自身),比如上面的 ababa,后缀子串为 babaababaa

  • 前缀子串:以某个字符串第一个字符为首字符的子串(不包含字符串自身),还是以 ababa 为例,前缀子串为 aabaabab

  • 最长可匹配后缀子串:后缀子串与前缀子串最长可匹配子串,也可叫做共有子串,以 ababa 为例,自然是 aba 了,长度为 3;

  • 最长可匹配前缀子串:与上面定义相对,即前缀子串与后缀子串最长可匹配子串。最长可匹配前缀子串和最长可匹配后缀子串肯定是一样的。

假设坏字符所在位置是 j,最长可匹配后缀子串长度为 k,则模式串需要后移的位数为 j-k。每当我们遇到坏字符,就将模式串后移 j-k 位,直到模式串与对应主串字符完全匹配;如果移到最后还是不匹配,则返回 -1。这就是 KMP 算法的核心思想。

二、实现原理

了解了核心思想,接下来,就可以考虑如何实现 KMP 算法了,实现 KMP 算法最核心的部分是构建一个用来存储模式串中每个前缀子串(这些前缀都有可能是好前缀)最长可匹配前缀子串的结尾字符下标数组,我们把这个数组叫做next 数组,对于上面 ababacd 这个模式串而言,对应的 next 数组如下:

KMP算法实现

其中,数组的下标是前缀子串结尾字符下标,数组的值是这个前缀的最长可匹配前缀子串的结尾字符下标。

有了这个 next 数组,我们就可以实现 KMP 匹配算法的核心代码了。

三、示例代码

下面是一个基于 KMP 算法实现的 Golang 版字符串查找函数:

package main

import "fmt"

// 生成 next 数组func generateNext(p string) []int {    m := len(p)    next := make([]int, m, m)    next[0] = -1    next[1] = 0    i, j := 0, 1 // 前缀子串、后缀子串起始位置    // 因为是通过最长可匹配前缀子串计算,所以 j 的值最大不会超过 m-1    for j < m - 1 {        if i == -1 || p[i] == p[j] {            i++            j++            // 设置当前最长可匹配前缀子串结尾字符下标            next[j] = i        } else {            // 如果 p[i] != p[j],回到上一个最长可匹配前缀子串结尾字符下标            i = next[i]        }    }    return next}

// KMP 算法实现函数func kmpSearch(s, p string) int {    n := len(s)  // 主串长度    m := len(p)  // 模式串长度    next := generateNext(p) // 生成 next 数组    i, j := 0, 0    for i < n && j < m {        // 如果主串字符和模式串字符不相等,        // 更新模式串坏字符下标位置为好前缀最长可匹配前缀子串尾字符下标        // 然后从这个位置重新开始与主串匹配        // 相当于前面提到的把模式串向后移动 j - k 位        if j == -1 || s[i] == p[j] {            i++            j++        } else {            j = next[j]        }    }    if j == m {       // 完全匹配,返回下标位置        return i - j    } else {        return -1    }}

// 基于 KMP 算法实现查找字符串子串函数func strStrV2(haystack, needle string) int {    // 子串长度=0    if len(needle) == 0 {        return 0    }    //主串长度=0,或者主串长度小于子串长度    if len(haystack) == 0 || len(haystack) < len(needle) {        return -1    }    // 子串长度=1时单独判断    if len(needle) == 1 {        i := 0        for ; i < len(haystack); i++ {            if haystack[i] == needle[0] {                return i            }        }        return -1    }

    // 其他情况走 KMP 算法    return kmpSearch(haystack, needle)}

func main() {    s := "Hello, 学院君!"    p := "学院君"    pos := strStrV2(s, p)    fmt.Printf("Find \"%s\" at %d in \"%s\"\n", p, pos, s)}

运行上述代码,打印结果如下,说明字符串查找函数可以正常工作:

四、性能分析

KMP 算法比 BF 算法实现起来要复杂的多,不过带来的好处是执行效率的提升,综合 KMP 算法实现函数和 next 数组生成函数,它的时间复杂度是 O(n+m),其中 n 是主串长度,m 是子串长度,m 和 n 的值越大,性能比 BF 算法更好,考虑到对于较长的主串,m 相对于 n 而言一般可以忽略,所以可以把 KMP 算法的时间复杂度近似看作 O(n)。

这个性能还是相当不错的,因此,KMP 算法被广泛用于字符串查找和匹配场景。

(本文完)


(0)

相关推荐

  • 字符串匹配算法详解

    希望看到文章的你们,能够在今年的研究生考试中超常发挥. 愿你们都能考上自己心仪的学校,为你们的备考生涯划上一个完美的句号.做为你们的师兄有几句话想对你们说,希望这些话能对你们有一些帮助. 马上就要考试 ...

  • Go 数据结构和算法篇(十一):字符串匹配之 BF 算法

    Go语言中文网 前天以下文章来源于xueyuanjun ,作者xueyuanjun xueyuanjun学院君的订阅号,我会在这里持续更新优质全栈编程技术教程,包括但不限于 Golang.PHP.Ja ...

  • Go 数据结构和算法篇(二):栈

    Go语言中文网 今天 以下文章来源于xueyuanjun ,作者xueyuanjun 从逻辑角度来说,数组和链表都是线性结构(就是排成一条线的结构,只有前后两个方向,非线性结构包括树.图等,后面会讲到 ...

  • 看野眼/篇十二:我從滿街的欲火焚心裡走來

    禮拜三黃昏六點半,與兩位弟弟言笑晏晏吃晚飯,春彥一隻電話打進來,開口就問妹妹儂在哪裡啊?來吃晚飯好嗎? 春彥,儂又拿我當應召女郎,全世界只有中國男人有這個惡習. 妹妹啊,正月裡,儂不要這麼兇啊,來吃飯 ...

  • 《星度指南》第五篇十二宫论

    原文地址:<星度指南>第五篇十二宫论 原文作者:襄阳金笑易 第五篇 十二宫论 论父母 宅为父母之宫,日月为父母之象.如火罗犯太阳,或难(木炁)克日及带剑锋,主先克父.如土计田侵月,或难星克 ...

  • 闲来挑灯论鞋 篇十二:他来了他来了,绝影䨻过来了

    官宣,8.10,绝影 自从李宁的*级竞速跑鞋,飞电和天马上市以来,䨻科技就备受竞速跑者的关注.事实上,在在推出这两双竞速跑鞋以后,李宁也没有停止动作.关于飞电二代小道消息一直甚嚣尘上.在今年年初,李宁 ...

  • 超轻粘土丨趣味玩法—谜语篇十二

    小朋友们,大家晚上好! 上期我们的谜语不少小朋友都猜出了谜底, 首先给大家公布一下谜底! 狐狸:身体纤瘦,毛长且厚.体长加尾长2到3英尺(60到90厘米).狐狸毛茸茸的尾巴是头部和身体的一半或2/3, ...

  • [转载]苍燃九宫盲派命理学习之基础篇十二    观自在编辑

    原文地址:苍燃九宫盲派命理学习之基础篇十二    观自在编辑 原文作者:观自在 苍燃论格局用神(二) 现在还有一个关于 "用神"的问题,在诸多书籍上都非常强调"用神&qu ...

  • 十二经脉太难记?请看这篇十二经脉解读汇总。内附歌诀、高清大图

    腑脏十二经穴起止歌 手肺少商中府起,大肠商阳迎香二. 足胃承泣厉兑三,脾部隐白大包四. 手心极泉少冲来,小肠少泽听宫去. 膀胱睛明至阴间,肾经涌泉俞府位. 心包天池中冲随,三焦关冲耳门继. 胆家瞳子髎 ...

  • 上海飯局 / 篇十二:鱘龍魚筵

    本埠老牌粵菜館子名豪,以鱘龍魚設題張筵,張敏小姐和范范小姐講,六月底,是鱘龍魚最後的旬季了,一年一度,僅此而已. 當日午筵,一大早,勞煩二位小姐陪伴,於虹梅路名豪樓上的私書房,瀏覽藏書,特別是飲食文化 ...