理论部分
kmp与解决的问题
关于kmp算法是由发明他的三个科学家的名字首字母组合而来的名称,主要是来解决字符串的匹配问题,比如文本串aabaabaaf和模式串aabaaf能否匹配上,
文本串 aabaabaaf 模式串 aabaaf (即模式串aabaaf是否是文本串aabaabaaf的一部分)的问题,主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。要解决这个问题,我们首先来了解字符串的前缀和后缀,最长相等前后缀和前缀表的概念
前缀和后缀
字符串前缀就是一个字符串的包含首字母但不包含尾字母的所有子串,列如字符串aabaaf前缀:
字符串前缀 aabaaf前缀1 a aabaaf前缀2 aa aabaaf前缀3 aab aabaaf前缀4 aaba aabaaf前缀5 aabaa 错误前缀 aabaaf(包含首字母但不包含尾字母的所有子串,包含尾字母,所以不是前缀) 由此可知,该字符串有5个前缀;
字符串后缀就是一个字符串的包含尾字母但不包含首字母的所有子串,列如字符串aabaaf后缀:
字符串后缀 aabaaf后缀1 f aabaaf后缀2 af aabaaf后缀3 aaf aabaaf后缀4 baaf aabaaf后缀5 abaaf 错误后缀 aabaaf(包含尾字母但不包含首字母的所有子串,包含首字母,所以不是后缀) 由此可知,该字符串有5个后缀;
最长相等前后缀和前缀表
理解了前缀和后缀的概念,最长相等前后缀就是一个字符串的前缀和后缀是相等且长度是最长的部分,比如我们来分析一下字符串aabaaf从开头a开始所有子串的最长相等前后缀:
最长相等前后缀 字符串 前缀 后缀 最长相等前后缀 a 无(因为既是首字母,又是尾字母) 无(因为既是首字母,又是尾字母) 无,最长相等前后缀长度为0 aa a a a,最长相等前后缀长度为1 aab a,aa b,ab 无,最长相等前后缀长度为0 aaba a,aa,aab a,ba,aba a,最长相等前后缀长度为1 aabaa a,aa,aab,aaba a,aa,baa,abaa aa,最长相等前后缀长度为2 aabaaf a,aa,aab,aaba,aabaa, f,af,aaf,baaf,abaaf 无,最长相等前后缀长度为0 根据上面每个字符串的最长相等前后缀长度,可以得到一个序列010120,该序列就是字符串aabaaf的前缀表,记录着每个字符串的最长相等前后缀长度
前缀表 a a b a a f 0 1 0 1 2 0
使用前缀表进行匹配
我们来使用前缀表来对文本串aabaabaaf和模式串aabaaf进行匹配
文本串 | a | a | b | a | a | b | a | a | f |
是否匹配 | 是 | 是 | 是 | 是 | 是 | 否 | |||
模式串 | a | a | b | a | a | f | |||
模式串前缀表 | 0 | 1 | 0 | 1 | 2 | 0 |
可以看到在模式串匹配 f 时没有匹配到,则要找到 f 前面的子串aabaa的最长相等前后缀的是什么,可以知道是 2 ,最现在从字符串下标为 2 的字符开始匹配,即模式串 b 开始,这是因为 f 后面有一个前缀aa, f 不匹配了,就要找和前缀aa相等的前缀后开始匹配,刚好就是下标为 2 的字符b,所以继续匹配为:
文本串 | a | a | b | a | a | b | a | a | f |
是否匹配 | 是 | 是 | 是 | 是 | |||||
模式串 | a | a | b | a | a | f | |||
模式串前缀表 | 0 | 1 | 0 | 1 | 2 | 0 |
由此可知,从字符 b 开始匹配后一直匹配成功,则文本串aabaabaaf和模式串aabaaf可以进行匹配,而这就是使用了KMP算法进行字符串匹配的操作。
代码部分
next数组的不同实现方法
现在代码中都喜欢使用next或者prefix来存储我们的前缀表,两种名称都是可以的,我们这里统一使用next来存储前缀表,我们使用next存储上面的模式串aabaaf的前缀表,而现在有三种主流的方式实现我们的next数组,第一种就是直接用前缀表当成next数组,当遇到不匹配的字符时,找到该字符前面的字符对应的next数组值来进行跳转到对应下标继续进行匹配(如不匹配字符 f 的前面的字符 a 的next数组数值2跳转到下标为2的字符b),即遇见冲突看前一位
模式串 | a | a | b | a | a | f |
next数组 | 0 | 1 | 0 | 1 | 2 | 0 |
第二种就是将前缀表整体右移动一格,在首部加上-1作为next数组,当遇到不匹配的字符时,将不匹配字符的next数组值来进行跳转到对应下标继续进行匹配(如不匹配字符 f 的的next数组数值2,跳转到下标为2的字符b),即遇见冲突看当前位
模式串 | a | a | b | a | a | f |
next数组 | -1 | 0 | 1 | 0 | 1 | 2 |
第三种就是将前缀表的所有值减去1来作为next数组当遇到不匹配的字符时,将不匹配字符前面的字符的next数组值加1来进行跳转到对应下标继续进行匹配(如不匹配字符 f 的前面的字符 a 的的next数组数值1加上1为2,跳转到下标为2的字符b),即遇见冲突看前一位加一
模式串 | a | a | b | a | a | f |
next数组 | -1 | 0 | -1 | 0 | 1 | -1 |
以上三种方法实现next数组都是可以的,我们在这里使用第一种(直接使用前缀表作为next数组)来实现具体的代码实现。
next数组具体代码实现
构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步:
- 初始化
- 处理前后缀不相同的情况
- 处理前后缀相同的情况
下面我就给大家来一一介绍:
初始化
我们要定义两个指针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数组代码篇)