文档讲解: 代码随想录
视频讲解: 《代码随想录》算法公开课-跟着Carl学算法
KMP算法
KMP算法适合解决什么样的问题?主要应用在字符串的匹配上。我们在匹配两个字符串时,当出现不匹配的字母,KMP思想就是知道一部分之前已经匹配的子串,可以利用这些已知信息避免从头再去做匹配。由next数组来记录之前已经匹配好的子串,接下来如何创建next数组是重中之重!
next数组就是前缀表,前缀表有什么作用?我们重新明确KMP的思想:利用这些已知信息避免从头再去做匹配。 也就是说前缀表的任务是:当前位置匹配失败,利用前缀表找到之前已经匹配的位置,然后再重新匹配!这也就意味着一旦在某个字符匹配失败,前缀表会告诉我们下一步应该跳到什么位置。
明白作用之后,继续讨论下一个问题:前缀表数组里面到底存储的是什么?每个元素记录的是当前位置之前的子串中,最长相等前后缀的长度!
接下来简单解释什么是前缀?什么是后缀?
- 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
- 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
我们前缀表记录的是最长相等前后缀的长度,也就是要对比每个位置之前的子串,它所对应的前缀和后缀中相等子子串的长度!
为什么前缀表这样设计?我们的目的是利用它来告诉我们一旦匹配失败下一步跳到什么位置,通过前缀和后缀中相等的子子串,我们就可以找到下一步要调的位置,而这个位置正好就是最长相等前后缀的长度!
了解前缀表的原理之后,思考接下来的问题,也是KMP算法的重点,如何通过程序来获得这个next[]数组?
理解起来感觉比较困难,下面记录下一刷时的理解:
构造next数组只要分为四个步骤:
- 数组的初始化
- 处理前后缀不同的情况
- 处理前后缀相同的情况
- 更新next数组的值
一、数组初始化
我们需要定义两个指针 i 和 j,其中 j 指向前缀末尾的位置(也是最长相等前后缀长度),i 指向后缀末尾的位置。给当前 j 赋初值 0,由于单个字母最长相等前后缀的为0,所以我们直接给next[0] 也赋值为0。指针 i 我们通过for循环进行常规遍历,由于next数组0索引位置的值已经被确定,所以这里 i 从1开始遍历。
二、处理前后缀不同的情况
当前 i 与 j位置处的字母不相同时,将j位置进行回退,回退到哪里取决于它next[j - 1],还不满足继续回退,因此需要用while循环,当然为了不超出数组范围,循环要加一条限定条件:j > 0。
三、处理前后缀相同的情况
当前 i 与 j位置处的字母相同时,将 j 位置+1,然后赋值给next[i],表示 i 位置之前子串的最长相等前后缀的长度!
LeetCode28. 找出字符串中第一个匹配项的下标
题目链接:28. 找出字符串中第一个匹配项的下标
题目描述:给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
利用java自带的函数处理起来非常简单:
class Solution {
public int strStr(String haystack, String needle) {
if (haystack.contains(needle)) {
return haystack.indexOf(needle);
} else
return -1;
}
}
利用KMP算法求解:
class Solution {
public int strStr(String haystack, String needle) {
int[] next = getNext(needle); // 获取needle的前缀表
// 遍历haystack字符串
for (int i = 0, j = 0; i < haystack.length(); i++) {
// 匹配不成功 j跳到相应位置
while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = next[j - 1];
}
// 匹配成功
if (j < needle.length() && needle.charAt(j) == haystack.charAt(i)) {
j++;
}
// 一旦j遍历到最后说明匹配成功
if (j == needle.length()) {
return i - needle.length() + 1;
}
}
return -1;
}
// 先定义求next数组函数
public int[] getNext(String s) {
int[] next = new int[s.length()]; // 动态初始化
int j = 0;
next[0] = 0;
for (int i = 1; i < s.length(); i++) {
// 前后缀不相等情况
while (s.charAt(j) != s.charAt(i) && j > 0) {
j = next[j - 1];
}
// 前后缀相等情况
if (s.charAt(j) == s.charAt(i)) {
j++;
}
next[i] = j; // 更新i位置处的值
}
return next;
}
}
LeetCode459. 重复的子字符串
题目链接:459. 重复的子字符串
题目描述:给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。
移动匹配思路:判断字符串a是否由重复子串组成,只要将两个s前后拼接在一起,新的字符串里面如果还会有s出现,就说明可以由重复子串组成。在代码实现中,需要去除新字符串的首字符和尾字符,这能够在判断新字符串是否还有s时,避免搜索原本的s的。
class Solution {
public boolean repeatedSubstringPattern(String s) {
StringBuilder sb = new StringBuilder(s).append(s);
String str = sb.subSequence(1, sb.length() - 1).toString(); // 掐头去尾
if (str.contains(s)) {
return true;
} else {
return false;
}
}
}