美文网首页数据结构与算法
KMP字符串匹配算法

KMP字符串匹配算法

作者: 一剑孤城 | 来源:发表于2018-01-15 15:32 被阅读130次
    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

    相关文章

      网友评论

        本文标题:KMP字符串匹配算法

        本文链接:https://www.haomeiwen.com/subject/bowkoxtx.html