KMP字符串匹配算法

KMP-BG.jpg

为什么要写KMP字符串匹配算法呢?因为近段时间在补数据结构和算法,然后重拾大学的《大话数据结构》,记录一下学习的进度。

什么是KMP算法?

KMP算法是一种字符串匹配算法,时间复杂度为 O(n+m)(n是文本的长度,m是模式字符串的长度),相比于传统的字符串匹配算法的时间负复杂度 O(n * m) 来说,大大提高字符串的匹配速度。

举个例子:
文本字符串 -> ABCABCDDDEABCDABHJJEEIDAEAENN
模式字符串 -> ABCDABC

在匹配前需要计算最大前缀和最大后缀的公共元素长度以及next数组(后面有说明):

模式串 A B C D A B C
最大前缀和后缀的公共元素长度 0 0 0 0 1 2 3
next数组 -1 0 0 0 0 1 2

假设文本字符串的下标为:i ∈ [0, pl-1](pl为文本字符串的长度),模式字符串的下标为:j ∈ [0, sl-1](sl为模式字符串的长度),则:

文本字符串 -> ABCABCDDDEABCDABHJJEEIDAEAENN
模式字符串 -> ABCDABC
KMP匹配的规则:
加入失配字符串的文字下表为j,则模式字符串往右移的位数为当前失配字符串的下标(已匹配的字符串长度)减去当前失配字符串的next数组对应的数值,此时模式匹配的下标j=next[j]。

ABCABCDDDEABCDABHJJEEIDAEAENN
ABCDABC

失配字符为A|D,所以
i = 3
j = 3
next[j] = 0
往右移动的位数 = i - next[j] = 3 - 0 = 3
j = next[j] = 0

ABCABCDDDEABCDABHJJEEIDAEAENN
   ABCDABC
继续匹配,直到j=sl-1或者i=pl-1,匹配结束。如果j=sl-1,则匹配成功,模式字符串在文本字符串的位置为:index = i - j,否则,匹配失败。

KMP算法的步骤

上面简单的介绍了一下KMP字符串匹配算法的使用,现在详情介绍一下KMP算法的具体步骤以及一些相关名词的意义。

最大前缀和后缀的公共元素长度计算。

模式字符串:ABCDABC

模式字符串的子串 前缀 后缀 最大的公共字符串 最大的公共字符串的长度
A 0
AB A B 0
ABC A, AB BC, C 0
ABCD A, AB, ABC BCD, CD, D 0
ABCDA A, AB, ABC, ABCD BCDA, CDA, DA, A A 1
ABCDAB A, AB, ABC, ABCD, ABCDA BCDAB, CDAB, DAB, AB, B AB 2
ABCDABC A, AB, ABC, ABCD, ABCDA, ABCDAB BCDABC, CDABC, DABC, ABC, BC, C ABC 3

所以,模式字符串的前缀后缀最大的公共元素长度为:

模式串 A B C D A B C
最大前缀和后缀的公共元素长度 0 0 0 0 1 2 3

那么?最大前缀和后缀的公共元素长度有什么用呢?

举个例子:
假如模式字符串在 j=6 的地方失配,那么我们需不需要从 j=0,重新开始匹配呢?答案是否定的。因为最大的已匹配的字符串长度为 6,最大的前缀后缀的公共元素长度为 2,也就是说,前缀和后缀有最大的公共元素AB,那么,AB这个元素在后缀是匹配过的,不需要重新匹配,所以,只需要从没有匹配过的下表开始就可以,下标需要往左移动的位数为 6-2=4,也就是 j=6-4=2,从下标 j=2C 开始匹配,不需要重新匹配,节省了时间,提高了匹配的效率。

可以得到一个模式字符串失配时往左需要移动位数的公式:
需要移动的位数(n) = 当前已匹配的字符串长度(j) - 当前失配字符串上一个字符串的最大前缀后缀的公共元素长度(next[j-1])

理论上有 最大前缀和后缀的公共元素长度 就可以计算模式字符串在匹配失败时需要往左移动的位数,那么,为什么还需要 next 数组?

什么是KMP算法一节里,可以看出,next数组最大前缀和后缀的公共元素长度 数值的关系,next数组 就是 最大前缀和后缀的公共元素长度 整体往后移一位,超出的去掉,不足的补-1。那么其实,前面得出来的公式就可以写成这样:
n = j - next[j]
到此,可以知道,next数组 就是为了简化公式而弄出来的。

现在再重新匹配一次《什么是KMP算法?》里的例子

文本字符串 -> ABCABCDDDEABCDABHJJEEIDAEAENN
模式字符串 -> ABCDABC
pl(文本字符串的长度)
sl(模式字符串的长度)
i(文本字符串的下标)
j(模式字符串的下标)
n(需要移动的位数)
模式串 A B C D A B C
最大前缀和后缀的公共元素长度 0 0 0 0 1 2 3
next数组 -1 0 0 0 0 1 2

第一次匹配失败:

第一次匹配失败.png
i = 3
j = 3
n = i - next[j] = 3 - 0 = 3
j = next[j] = 0
所以,从文本字符串i=3,模式字符串j=0开始重新匹配。

第二次匹配

第二次匹配.png

i = 7
j = 4
n = i - next[j] = 7 - 0 = 7
j = next[j] = 0
所以,从文本字符串i=7,模式字符串j=0开始重新匹配。

直到最后i=pl-1时,j != sl-1,匹配失败。

使用代码实现KMP算法

首先,需要计算模式字符串的next数组。
很显然,j = 0时,next[0] = -1;
那么?怎么从next[j]推导next[j+1]呢?

假如j = 5,我们知道next[j] = 1,s[next[j]] = B,也就是最大的公共元素是AB,假如s[next[j] + 1] = s[j + 1], 那么可以得出,前缀和后缀最大的公共元素是ABC, 则next[j + 1] = 2。这里可以得出,当s[next[j] + 1] = s[j + 1]则,next[j + 1] = next[j] + 1。如果s[next[j] + 1] != s[j + 1]呢?那么,我们假定存在一个比ABC小的最大前缀后缀公共元素,需要不断递归,直到s[next[next[j]] + 1] = s[j + 1]。如果存在k,使得s[next[k] + 1] = s[j + 1],则next[j + 1] = next[k] + 1;如果不存在,则next[j + 1] = 0。

代码如下:

    //KMP计算next数组
    func nextTable(_ matchStr: String) -> [Int] {
        guard matchStr.count > 0 else {
            return []
        }
        
        let length = matchStr.count
        var next: [Int] = [-1]
        var k = -1
        var j = 0
        while j < length - 1 {
            //p[k]表示前缀,p[j]表示后缀
            if k == -1 || matchStr.subStrAtIndex(j) == matchStr.subStrAtIndex(k) {
                k = k + 1
                j = j + 1
                next.insert(k, at: j)
            } else {
                k = next[k]
            }
        }
        return next
    }

当k=-1表示j=0,此时next[j+1]=k+1=0
当s[j]=s[k]表示找到了,此时next[j+1]=next[k]+1

获得模式字符串的next数组,可以开始匹配了,代码如下:

    //KMP,字符串匹配算法 => O(n+m)
    func matchSubStr(_ subStr: String) -> (Bool, Int) {
        guard !subStr.isEmpty else {
            return (false, -1)
        }
        
        let next: [Int] = self.nextTable(subStr)
        
        var i = 0  //文本串
        var j = 0  //模式串
        let length = self.count
        let matchLenghth = subStr.count
        
        while i < length && j < matchLenghth {
            if j == -1 || self.subStrAtIndex(i) == subStr.subStrAtIndex(j) {
                i = i + 1
                j = j + 1
            } else {
                j = next[j]
            }
        }
        
        if j == matchLenghth {
            return (true, i - j)
        } else {
            return (false, -1)
        }
    }

KMP字符串匹配算法要点在于减少比不要的匹配次数,提高匹配效率。

BM算法和Sunday算法

KMP算法,已经在字符串匹配的效率上提升了一个级别,然而,没有最快的算法,只有更快的算法。BM算法的时间复杂度是 O(n) 级别的,而Sunday算法比BM算法效率更高。意不意外,惊不惊喜。大神的境界远不是我等凡人可以揣摩的,致敬。😂

最后

献上Swift写的KMP-Algorithm-Swift-Demo

参考文献:
极客学院:KMP算法
阮一峰:字符串匹配的KMP算法
The Knuth-Morris-Pratt Algorithm in my own words

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