KMP算法浅析

117 阅读3分钟

简介

KMP算法,全名Knuth-Morris-Pratt算法,是以其三个发布者命名的用于在字符串中查找子串的算法。

KMP算法事实上是一种由暴力匹配改进而来的算法,它可以在O(M+N)的时间复杂度和O(N)的时间复杂度下解决在字符串中查找字串的问题。其中心思想就是利用前缀函数求得的最长相等真前缀真后缀长度来跳过部分匹配项目

概念解释

后缀

后缀 是指从某个位置ii开始到整个串末尾结束的一个特殊子串。

真后缀 真后缀是指除了字符串SS本身的后缀

举例来说,字符串 abcabcd 的所有后缀为 {d, cd, bcd, abcd, cabcd, bcabcd, abcabcd},而它的真后缀为 {d, cd, bcd, abcd, cabcd, bcabcd}

前缀

前缀 是指从串首开始到某个位置ii结束的一个特殊子串。

真前缀 指除了字符串SS本身的的前缀。

举例来说,字符串 abcabcd 的所有前缀为 {a, ab, abc, abca, abcab, abcabc, abcabcd}, 而它的真前缀为 {a, ab, abc, abca, abcab, abcabc}

前缀函数

给定一个长度为nn的字符串,其前缀函数被定义为一个长度为nn的数组π\pi。其中π[i]\pi[i]定义是

  1. 如果子串s[0...i]s[0...i]有一对相等的真前缀与真后缀,那么π[i]\pi[i]就是这个相等真前缀与真后缀的长度。
  2. 如果不止有一对,那么π[i]\pi[i]就是其中最长一对的长度。
  3. 如果没有相等的,那么π[i]=0\pi[i] = 0

简单来说π[i]\pi[i]就是s[0...i]s[0...i]最长的相等的真前缀与真后缀的长度

KMP算法对比暴力求解

暴力求解

function getSubstrForce(haystack,needle) {
    let length = haystack.length
    let j = 0;
    let endLength = needle.length
    for( let i = 0; i < length; i++ ) {
        while(haystack[i] === needle[j]) {
            i++;
            j++
        }
        
        if(j === endLength) {
            return i - j
        } else {
            i -= j - 1
            j = 0
        }
    }
}

IMG_0011.png

KMP求解

我们分两种情况分析字符串匹配的过程,来优化暴力求解中存在的问题。

模式串中无重复

这种情况我们还以目标串ABCABDABCEABD和模式串ABCE为例,当第一次匹配失败时,我们可以发现A其实不必逐个从A->-B->C尝试,因为A和E中间已经匹配,肯定不是A,因为目标串不需要回退

模式串中有重复

IMG_0012.png

模式串中ABCAB均已匹配成功但是E与A不匹配,在这种情况下目标串仍不用回退,只要模式串开头的AB和目标串当前结尾的AB对齐,就可以从C中开始比较,又跳过了暴力比较中的两次比较。可以明显看出,模式串应回退截止已匹配字符串最大相等真前缀和真后缀的长度。

那么问题就移动到如何高效的求s[0...i]s[0...i]的最大相等真前缀与真后缀,也就是前缀函数如何生成

前缀函数生成

暴力求法

function getPrefixFun(input) {
    const length = input.length;
    let prefix = new Array(length).fill(0)
    for(let i = 1; i < lengthl; i++) {
        for(let j = i; j >= 0; j--) {
            if(input.substr(0,j) === input.substr(i - j + 1,j)) {
                prefix[i] = j
                break;
            }
        }
    }
    return prefix
}

优化

IMG_0013.png

当我们发现s[π[i]]s[i+1]s[\pi[i]] \neq s[i+1]的时候我们要找到一个最大长度j使得s[i+1]=s[j]s[i+1] = s[j],这时候观察我们可以发现j也是s[0...π[i]1]s[0...\pi[i] - 1]的相等真前后缀,所以可以得到s[π[i]j...π[i]1]=s[0...j1]s[\pi[i] - j...\pi[i] - 1] = s[0...j-1],所以我们可以得到j就是[0...π[i]1][0...\pi[i] - 1]处的前缀值,也是j=π[π[i]1]j = \pi[\pi[i] - 1]

const getNext = (input) => {
    const length = input.length
    let next = new Array(length).fill(0)
    for(let i = 1; i < length; i++) {
        let j = next[i - 1]
        while(j > 0 && input[i] !== input[j]) {
            j = next[j - 1]
        }
        if(input[i] === input[j])j++
        next[i] = j
    }
    return next
}

完整代码

var strStr = function(haystack, needle) {
    const getNext = (input) => {
        const length = input.length
        let next = new Array(length).fill(0)
        for(let i = 1; i < length; i++) {
            let j = next[i - 1]
            while(j > 0 && input[i] !== input[j]) {
                j = next[j - 1]
            }
            if(input[i] === input[j])j++
            next[i] = j
        }
        return next
    }
    let j = 0;
    let next = getNext(needle)
    for(let i = 0; i < haystack.length; i++) {
        while(j > 0 && haystack[i] !== needle[j]) {
            j = next[j-1]
        }
        if(haystack[i] === needle[j]) {
            j++
        }
        if(j === needle.length) {
            return i - j + 1
        }
    }

    return -1
};