从暴力匹配到KMP算法

101 阅读3分钟

前言

如何在一个文本串中找到模式串中最早出现的位置?

看到这个问题,我们最容易想到的方法就是暴力遍历。

function BF(txt,pat){
    let n=txt.length
    let m=pat.length
    for(let i=0;i<=n-m;i++){
        let j=0
        for(j=0;j<m;j++){
            if(pat[j]!=txt[i+j]){
                break
            }
        }
        if(j===m) return i
    }
    return -1
}

在匹配过程中,如果出现不匹配的字符,同时回退txt和pat的指针,该时间复杂度O(mn),空间复杂度O(1),如果字符串和模式串的长度较长则效果会非常不好。

KMP流程

利用之前判断过的信息,通过next数组保存模式串中前后最长公共子序列的长度,每次回溯的时候,通过next数组找到前面匹配过的位置,从而节省时间。

字符串的最长公共前后缀

字符串前缀:指不包含最后一个字符的所有以第一个字符开头的连续字串

字符串'ABABA' 的前缀有:A,AB,ABA,ABAB

字符串后缀:指不包含第一个字符的所有以最后一个字符结尾的连续字串

字符串'ABABA' 的后缀有:BABA,ABA,BA,A

公共前后缀:一个字符串的 所有前缀连续字串 和 所有后缀连续字串 中相等的字串

字符串:'ABABA'
前缀:A,AB,ABA,ABAB
后缀:BABA,ABA,BA,A
公共前缀:A,ABA

最长公共前后缀:所有公共前后缀中长度最长的字串

字符串:'ABABA',公共前后缀:A,ABA,最长公共前后缀:'ABA'

部分匹配表Next

对于字符串str,从第一个字符开始的每个字串的最后一个字符与该字串的最长公共前后缀的长度对于的关系表,就是next表。 以字符串'ABCABD'为例

  • 字串'A':最后一个字符是A,该字串的最长公共前后缀的长度是0,因此对于关系就是A-0
  • 字串'AB':最后一个字符是B,该字串的最长公共前后缀长度是0,因此对应关系就是B-0
  • 字串'ABC':最后一个字符是C,该字串的最长公共前后缀长度是0,因此对应关系就是C-0
  • 字串'ABCA':最后一个字符是A,该字串的最长公共前后缀长度是1,因此对应关系就是A-1
  • 字串'ABCAB':最后一个字符是B,该字串的最长公共前后缀长度是2,因此对应关系就是B-2
  • 字串'ABCABD':最后一个字符是D,该字串的最长公共前后缀长度是0,因此对应关系就是D-0

对应next数组为:[0,0,0,1,2,0]

// 构建KMP算法中的next数组(前缀表)
function getNext(str) {
    // 初始化next数组,用于存储每个位置的最长公共前后缀长度
    const next = new Array(str.length).fill(0)
    let maxPrefix = 0 // 当前最长匹配前缀长度
    
    next[0] = 0 // 第一个字符没有前缀,固定为0
    
    // 从第二个字符开始构建next数组
    for (let i = 1; i < str.length; i++){
        // 当字符不匹配时,利用已计算的next值回溯
        while (maxPrefix > 0 && str.charAt(i) !== str.charAt(maxPrefix)) {
            maxPrefix = next[maxPrefix - 1] // 关键回退步骤
        }
        
        // 找到匹配的前缀,延长最大匹配长度
        if (str.charAt(i) === str.charAt(maxPrefix)) {
            maxPrefix++
        }
        
        // 记录当前位置的最长公共前后缀长度
        next[i] = maxPrefix
    }
    return next
}

我们构建完next表后,就已经完成一半了。接着就是对字符串和模板串进行匹配。

// KMP主搜索函数
function KMPSearch(text, pattern) {
    if (pattern.length === 0) return 0; // 空模式直接返回
    
    const next = getNext(pattern); // 获取预处理好的next数组
    let j = 0; // 模式串指针
    
    // 遍历文本串(只需单次遍历,无需回溯)
    for (let i = 0; i < text.length; i++) {
        // 当发生不匹配时,通过next数组调整模式串位置
        while (j > 0 && text[i] !== pattern[j]) {
            j = next[j - 1]; // 关键跳跃步骤
        }
        
        // 当前字符匹配成功
        if (text[i] === pattern[j]) {
            j++;
        }
        
        // 完全匹配模式串
        if (j === pattern.length) {
            return i - j + 1; // 返回文本串中的起始位置
        }
    }
    return -1; // 未找到匹配项
}