Kmp 算法基本实现

75 阅读3分钟

I. KMP算法概述

KMP是一种字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,并由他们名字命名Knuth,Morris,Pratt。因此得名KMP。
KMP算法的是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。其时间复杂度为O(m+n)。

II. KMP算法原理

KMP算法,是在暴力算法的基础上,减少重复匹配,来实现的。先看下暴力匹配的过程,以及暴力匹配的问题。 有如下主串,需要匹配模式串主串中的位置
主串:a,b,a,b,a,b,c,a,a
模式串:a,b,a,b,c

通过暴力匹配的方式如下:

截屏2023-10-20 17.02.40.png

从图中可以看到,在失配的情况下,主串需要进行回溯再进行匹配。未利用已经匹配过的数据信息,减少匹配。而KMP则可以避免主串回溯。

截屏2023-10-20 17.04.30.png

从中,可以看出通过KMP可以减少匹配次数。

KMP中模式串下方的数组,用来在失配的时候,标识模式串匹配的下标,即回溯的位置,称之为next数组。而其计算的过程为预处理阶段,后续的模式匹配称为搜索阶段。

A. 预处理阶段

next数组主要用于控制模式串的回溯位置,如果模式串有相同项,利用该信息,就可以减少重复的回溯。

截屏2023-10-23 15.40.36.png

主串和模式串,在第四位,出现失配的情况。模式串前两位ab和开头的ab相同,长度为2。那么主串的前四位也是如此。利用该信息,就可以减少模式串的回溯,直接从第二位开始匹配即可。因此,通过匹配字符串前面相等前后缀的方式,得到不同索引下的回溯长度。

当搜索第0位时,0位之前不存在,设置为 -1。其他位为匹配前面的字符串最长相等前后缀

索引
0null
1a
2ab
3aba
4abab

索引为0,之前不存在字符串,长度设置为 -1

索引1,之前字符串a
由于前后缀都为0。因此,最长相等前后串长度0

索引2,之前字符串a,b
前缀:['a']
后缀:['b']
由于前后缀不相等。因此,最长相等前后串长度0

索引3,之前字符串a,b,a
前缀:['a','a,b']
后缀:['a','b,a']
由上可以看出,最长相等前后串长度1

索引4,之前字符串a,b,a,b
前缀:['a','a,b','a,b,a']
后缀:['b','a,b', 'b,a,b' ]
由上可以看出,最长相等前后串长度2

这个时候,得到next数组[-1,0,0,1,2],由于索引0 ,之前字符串不存在,索引 1 ,之间只有1个字符串。因此,next数组以[-1,0]开头。

下面看下代码中,next数组如何生成的。

截屏2023-10-27 20.24.26.png

function getNext(p) {
    let next = [];
    let j = 0; // 前缀末尾
    next[0] = -1; // 0之前没有字符串,长度为 -1
    next[1] = 0; //  1 之前,前后缀最长长度为 0

    //  i 后缀末尾
    for (let i = 1; i < p.length - 1; i++) {
        // 不匹配
        while (j != 0 && p[i] !== p[j]) {
            j = next[j]
        }

        // 匹配
        if (p[i] == p[j]) {
            ++j;
        }
        next[i + 1] = j
    }

    return next;
}

B. 搜索阶段

在获取到next数组后, 在失配的时候根据next数组值,进行模式串回退处理,主要流程如下

截屏2023-10-20 17.31.37.png

III. KMP算法实现

A. Javascript实现


function kmp(mainString, pattern) {

    let next = getNext(pattern);

    let i = 0;
    let k = 0

    while (i < mainString.length) {
        if (pattern[k] === mainString[i]) {
            k++;
            i++;
        }
        else {
            if (k > 0) {
                k = next[k]
            }
            else {
                i++
            }
        }

        if (k === pattern.length - 1) {
            console.log("找到索引" + (i - k));
            k = next[k];
        }
    }
}

function getNext(p) {
    let next = [];
    let j = 0; // 前缀末尾
    next[0] = -1; // 0之前没有字符串,长度为 -1
    next[1] = 0; //  1 之前,前后缀最长长度为 0

    //  i 后缀末尾
    for (let i = 1; i < p.length - 1; i++) {
        // 不匹配
        while (j != 0 && p[i] !== p[j]) {
            j = next[j]
        }

        // 匹配
        if (p[i] == p[j]) {
            ++j;
        }
        next[i + 1] = j
    }

    return next;
}