帮你把KMP算法学个通透!(理论加代码篇)

理论部分

kmp与解决的问题

  关于kmp算法是由发明他的三个科学家的名字首字母组合而来的名称,主要是来解决字符串的匹配问题,比如文本串aabaabaaf模式串aabaaf能否匹配上,

文本串 aabaabaaf
模式串 aabaaf

(即模式串aabaaf是否是文本串aabaabaaf的一部分)的问题,主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。要解决这个问题,我们首先来了解字符串的前缀和后缀最长相等前后缀前缀表的概念

前缀和后缀

  字符串前缀就是一个字符串的包含首字母不包含尾字母所有子串,列如字符串aabaaf前缀:

字符串前缀
aabaaf前缀1a
aabaaf前缀2aa
aabaaf前缀3aab
aabaaf前缀4aaba
aabaaf前缀5aabaa
错误前缀aabaaf(包含首字母但不包含尾字母的所有子串,包含尾字母,所以不是前缀)

  由此可知,该字符串有5个前缀;

  字符串后缀就是一个字符串的包含尾字母不包含首字母所有子串,列如字符串aabaaf后缀:

字符串后缀
aabaaf后缀1f
aabaaf后缀2af
aabaaf后缀3aaf
aabaaf后缀4baaf
aabaaf后缀5abaaf
错误后缀aabaaf(包含尾字母但不包含首字母的所有子串,包含首字母,所以不是后缀)

  由此可知,该字符串有5个后缀;

最长相等前后缀和前缀表

  理解了前缀和后缀的概念,最长相等前后缀就是一个字符串的前缀和后缀是相等长度是最长的部分,比如我们来分析一下字符串aabaaf从开头a开始所有子串的最长相等前后缀:

最长相等前后缀
字符串前缀后缀最长相等前后缀
a无(因为既是首字母,又是尾字母)无(因为既是首字母,又是尾字母)无,最长相等前后缀长度为0
aaaaa,最长相等前后缀长度为1
aaba,aab,ab无,最长相等前后缀长度为0
aabaa,aa,aaba,ba,abaa,最长相等前后缀长度为1
aabaaa,aa,aab,aabaa,aa,baa,abaaaa,最长相等前后缀长度为2
aabaafa,aa,aab,aaba,aabaa,f,af,aaf,baaf,abaaf无,最长相等前后缀长度为0

根据上面每个字符串的最长相等前后缀长度,可以得到一个序列010120,该序列就是字符串aabaaf的前缀表,记录着每个字符串的最长相等前后缀长度

前缀表
aabaaf
010120

使用前缀表进行匹配

   我们来使用前缀表来对文本串aabaabaaf模式串aabaaf进行匹配

使用前缀表匹配过程
文本串aabaabaaf
是否匹配
模式串aabaaf
模式串前缀表010120

可以看到在模式串匹配 时没有匹配到,则要找到 f 前面的子串aabaa最长相等前后缀的是什么,可以知道是 2 ,最现在从字符串下标为 2 的字符开始匹配,即模式串 b 开始,这是因为 f 后面有一个前缀aa, f 不匹配了,就要找和前缀aa相等的前缀后开始匹配,刚好就是下标为 2 的字符b,所以继续匹配为:

使用前缀表进行匹配
文本串aabaabaaf
是否匹配
模式串aabaaf
模式串前缀表010120

由此可知,从字符 b 开始匹配后一直匹配成功,则文本串aabaabaaf模式串aabaaf可以进行匹配,而这就是使用了KMP算法进行字符串匹配的操作。

代码部分 

next数组的不同实现方法

  现在代码中都喜欢使用next或者prefix存储我们的前缀表,两种名称都是可以的,我们这里统一使用next存储前缀表,我们使用next存储上面的模式串aabaaf前缀表,而现在有三种主流的方式实现我们的next数组第一种就是直接用前缀表当成next数组,当遇到不匹配的字符时,找到该字符前面的字符对应的next数组值来进行跳转到对应下标继续进行匹配(如不匹配字符 f 前面的字符 a 的next数组数值2跳转到下标为2的字符b),即遇见冲突看前一位

前缀表当成next数组
模式串aabaaf
next数组010120

第二种就是将前缀表整体右移动一格,在首部加上-1作为next数组,当遇到不匹配的字符时,将不匹配字符的next数组值来进行跳转到对应下标继续进行匹配(如不匹配字符 f 的的next数组数值2,跳转到下标为2的字符b),即遇见冲突看当前位

前缀表右移一格作为next数组
模式串aabaaf
next数组-101012

第三种就是将前缀表的所有值减去1来作为next数组当遇到不匹配的字符时,将不匹配字符前面的字符的next数组值加1来进行跳转到对应下标继续进行匹配(如不匹配字符 f 前面的字符 a 的的next数组数值1加上1为2,跳转到下标为2的字符b),即遇见冲突看前一位加一

前缀表值减去1作为next数组
模式串aabaaf
next数组-10-101-1

以上三种方法实现next数组都是可以的,我们在这里使用第一种(直接使用前缀表作为next数组)来实现具体的代码实现。

next数组具体代码实现 

构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步: 

  1. 初始化
  2. 处理前后缀不相同的情况
  3. 处理前后缀相同的情况

下面我就给大家来一一介绍:

初始化

  我们要定义两个指针i,j分别指向后缀的末尾和前缀的末尾,其中j不仅表示指向前缀末尾,还表示该字符串的最大相同前后缀的长度,所以我们初始化j=0表示只有一个字符时指向的前缀末尾,同时表示只有一个字符时最大相同前后缀的长度为0,next[0] = 0;而指针i定义在for循环中从1指向字符串s的末尾,因为字符串长度为二时才有后缀末尾

function(next,s){ //定义next数组和对应的字符串s
let j = 0;
next[0] = 0;
for(let i = 1;i < s.length;i++){
}
}

 处理前后缀不相同的情况

  当前后缀不相同时,即字符串出现不匹配时(即s[i]!==s[j]),我们前面说过,遇到冲突看前一位,所以前缀末尾指针j就要跳转前一位的next数组值,即j = next[j-1],j要一直跳转直到前后缀匹配相等,但是j不能一直跳转,最多跳转到字符串的第一位,所以j>0;所以要添加条件while(j>0&&s[i]!==s[j]){ j = next[j-1] }

function(next,s){ //定义next数组和对应的字符串s
let j = 0;
next[0] = 0;
for(let i = 1;i < s.length;i++){
   while(j>0&&s[i]!==s[j]){
        j = next[j-1]// 回退到前一个相等前后缀的末尾
}

}
}

处理前后缀相同的情况

 当前后缀相等时(即s[i]===s[j]),说明此时最大相等前后缀的长度加一了,所以要让j=j+1(j也为最大相等前后缀长度),next[i] = j

function getNext(next, s) {
    let j = 0; // 前缀末尾和最大相等前后缀长度
    next[0] = 0;
    for (let i = 1; i < s.length; i++) { // i 为后缀末尾
        while (j > 0 && s[i] !== s[j]) {
            j = next[j - 1]; // 回退到前一个相等前后缀的末尾
        }
        if (s[i] === s[j]) {
            j++; // 匹配长度加一
        }
        next[i] = j; // 记录当前后缀的最长相等前后缀长度
    }
}

最后我们就得到了next数组实现的详细代码

KMP代码实战(leetcode.28)

  接下来我们运用kmp算法来解决leetcode的一道金典的字符串匹配问题

题目问题就是要我们给出匹配字符串开始的第一个下标,如果没有就返回-1

/**
 * 计算字符串的 next 数组,用于 KMP 算法
 * @param {number[]} next - 存储 next 值的数组
 * @param {string} s - 原始字符串
 */
function getNext(next, s) { //得到next数组的函数
    let j = 0; // 前缀末尾和最大相等前后缀长度
    next[0] = 0;
    for (let i = 1; i < s.length; i++) { // i 为后缀末尾
        while (j > 0 && s[i] !== s[j]) {
            j = next[j - 1]; // 回退到前一个相等前后缀的末尾
        }
        if (s[i] === s[j]) {
            j++; // 匹配长度加一
        }
        next[i] = j; // 记录当前后缀的最长相等前后缀长度
    }
}

/**
 * 实现字符串查找算法(KMP 算法)
 * @param {string} haystack - 原始字符串
 * @param {string} needle - 需要查找的子字符串
 * @return {number} - 子字符串在原始字符串中的起始位置,若未找到则返回 -1
 */
var strStr = function(haystack, needle) {
    if (needle.length === 0) {
        return 0; // 空字符串是任何字符串的子串,返回 0
    }
    let next = new Array(needle.length).fill(0); // 初始化 next 数组
    getNext(next, needle);
    let j = 0; // j 是 needle 的索引
    for (let i = 0; i < haystack.length; i++) { // i 是 haystack 的索引
        while (j > 0 && haystack[i] !== needle[j]) {
            j = next[j - 1]; // 回退到下一个可能的匹配位置,遇到冲突看前一位
        }
        if (haystack[i] === needle[j]) {
            j++; // 匹配字符,继续下一个字符的比较
        }
        if (j === needle.length) {
            return i - needle.length + 1; // 完全匹配,返回起始位置
        }
    }
    return -1; // 未找到匹配
};

其他版本代码

C++

class Solution {
public:
    void getNext(int* next, const string& s) {
        int j = 0;
        next[0] = 0;
        for(int i = 1; i < s.size(); i++) {
            while (j > 0 && s[i] != s[j]) {
                j = next[j - 1];
            }
            if (s[i] == s[j]) {
                j++;
            }
            next[i] = j;
        }
    }
    int strStr(string haystack, string needle) {
        if (needle.size() == 0) {
            return 0;
        }
        int next[needle.size()];
        getNext(next, needle);
        int j = 0;
        for (int i = 0; i < haystack.size(); i++) {
            while(j > 0 && haystack[i] != needle[j]) {
                j = next[j - 1];
            }
            if (haystack[i] == needle[j]) {
                j++;
            }
            if (j == needle.size() ) {
                return (i - needle.size() + 1);
            }
        }
        return -1;
    }
};

Java

public class Solution {
    public void getNext(int[] next, String s) {
        int j = 0;
        next[0] = 0;
        for (int i = 1; i < s.length(); i++) {
            while (j > 0 && s.charAt(i) != s.charAt(j)) {
                j = next[j - 1];
            }
            if (s.charAt(i) == s.charAt(j)) {
                j++;
            }
            next[i] = j;
        }
    }

    public int strStr(String haystack, String needle) {
        if (needle.length() == 0) {
            return 0;
        }
        int[] next = new int[needle.length()];
        getNext(next, needle);
        int j = 0;
        for (int i = 0; i < haystack.length(); i++) {
            while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
                j = next[j - 1];
            }
            if (haystack.charAt(i) == needle.charAt(j)) {
                j++;
            }
            if (j == needle.length()) {
                return i - needle.length() + 1;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        String haystack = "hello";
        String needle = "ll";
        System.out.println(solution.strStr(haystack, needle));  // Output: 2
    }
}

如果同学们还有疑问,可以结合这两个视频观看:帮你把KMP算法学个通透!(理论篇)帮你把KMP算法学个通透!(求next数组代码篇)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值