本次两道题都是KMP相关 马上毕设中期,后续补充
在一个串中查找是否出现过另一个串,这是KMP的看家本领。
KMP的思想:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再做匹配。
本篇将以如下顺序来讲解KMP,
- 什么是KMP
- KMP有什么用
- 什么是前缀表
- 为什么一定要用前缀表
- 如何计算前缀表
- 前缀表与next数组
- 使用next数组来匹配
- 时间复杂度分析
- 构造next数组
- 使用next数组来做匹配
- 前缀表统一减一 C++代码实现
- 前缀表(不减一)C++实现
- 总结
28.实现 strStr(()
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
。
示例 1:
输入:haystack = "sadbutsad", needle = "sad" 输出:0 解释:"sad" 在下标 0 和 6 处匹配。 第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入:haystack = "leetcode", needle = "leeto" 输出:-1 解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
思路
本题算是KMP最经典的运用,在一个串里查找是否出现过另一个串。
常规迭代也可以解决
常规迭代
遍历文本串,当出现不匹配时 文本串回退已匹配的长度,模式串回退到开头位置。
class Solution {
/**
* 基于窗口滑动的算法
* <p>
* 时间复杂度:O(m*n)
* 空间复杂度:O(1)
* 注:n为haystack的长度,m为needle的长度
*/
public int strStr(String haystack, String needle) {
int m = needle.length();
// 当 needle 是空字符串时我们应当返回 0
if (m == 0) {
return 0;
}
int n = haystack.length();
if (n < m) {
return -1;
}
int i = 0;
int j = 0;
while (i < n - m + 1) {
// 找到首字母相等
while (i < n && haystack.charAt(i) != needle.charAt(j)) {
i++;
}
if (i == n) {// 没有首字母相等的
return -1;
}
// 遍历后续字符,判断是否相等
i++;
j++;
while (i < n && j < m && haystack.charAt(i) == needle.charAt(j)) {
i++;
j++;
}
if (j == m) {// 找到
return i - j;
} else {// 未找到
i -= j - 1;
j = 0;
}
}
return -1;
}
}
KMP(-1)
kmp解法主要解决两个问题,一是构造next数组,二是根据next数组去匹配
构造next数组
我们定义一个函数getNext来构建next数组,函数参数为next数组,和一个字符串。 代码如下:
public void getNext(int [] next, String s){
}
构造next数组其实就是计算模式串s,前缀表的过程,分为三步
1、初始化
2、处理前后缀不相同的情况
3、处理前后缀相同的情况
详解:
1、 初始化:
定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置,然后对next数组进行初始化赋值,如下:
int j = -1;
next[0] = j;
j 为什么要初始化为 -1呢,因为 前缀表要统一减一的操作仅仅是其中的一种实现,我们这里选择 j 初始化为-1,下文我还会给出 j 不初始化为-1的实现代码。
next[i] 表示 i (包括i)之前最长相等前后缀长度 (其实就是 j )
所以初始化next[0] = j
2、处理前后缀不相同的情况
因为j初始化为-1,那么 i 就从 1 开始,进行 s[i] 与 s [j + 1] 的比较
所以遍历模式串 s 的循环下标 i 要从 1 开始,代码如下:
for(int i = 1; i < s.length(); i++)
如果s[i] 与 s[j+1] 不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退,
怎么回退呢?
next[j] 就是记录着j (包括j)之前的子串的相同前后缀的长度
那么s[i] 与s[j + 1]不相同,就要找 j+1 前一个元素在next数组里的值(就是next[j])
所以,处理前后缀不相同情况的代码如下:
//处理前后缀不相等的情况
while(j >= 0 && next[i] != next[j+1]){
j = next[j]; //向前回退
}
3、处理前后缀相同情况
如果 s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。
代码如下
//处理前后缀相等的情况
if(next[i] == next[j+1]){
j++;
}
next[i] = j;
所以,构造next数组的函数代码如下:
public void getNext(int [] next, String s){
// j 指向前缀末尾位置 i 指向后缀末尾位置
// next[i] 表示 i (包括i) 之前最长相等的前后缀长度(其实就是 j)
int j = -1;
next[0] = j;
for(int i = 1; i < s.length(); i++){
//处理前后缀不相等的情况
while(j >= 0 && next[i] != next[j+1]){
j = next[j]; //向前回退
}
//处理前后缀相等的情况
if(next[i] == next[j+1]){
j++;
}
next[i] = j;
}
}
根据next数组去匹配:
在文本串s里 找是否出现过模式串t。
定义两个下标 i 和 j ,j 指向模式串起始位置,i指向文本串起始位置。
那么j初始值依然为-1,为什么呢? 依然因为next数组里记录的起始位置为-1。
i就从0开始,遍历文本串,代码如下:
for(int i = 0; i < haystack.length(); i++)
接下来就是文本串与模式串的比较(s[i] 与 t[j + 1] j 从 -1 开始)
如果 不相同 j就要从next数组里找下一个匹配的位置
代码如下
while(j >= 0 && haystack.charAt(i) != needle.charAt(j + 1)){
j = next[j];//回退
}
如果 s[i] 与 t[j + 1] 相同,那么i 和 j 同时向后移动, 代码如下:
if(haystack.charAt(i) == needle.charAt(j + 1)){
j++;
}
如何判断在文本串s里出现了模式串t呢,如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。
本题要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。
代码如下:
if(j == needle.length() - 1){
return i - needle.length() + 1;
}
那么,利用next数组,使用模式串匹配文本串的整体代码如下:
int j = -1;//因为next数组里记录的初始位置为-1
for(int i = 0; i < haystack.length(); i++){//i从0开始
//不匹配
while(j >= 0 && haystack.charAt(i) != needle.charAt(j + 1)){
j = next[j];//回退,j寻找之前匹配的位置
}
//匹配,i与j同时向后移动
if(haystack.charAt(i) == needle.charAt(j + 1)){
j++;
}
//文本串里出现了模式串
if(j == needle.length() - 1){
return (i - needle.length() + 1);
}
}
代码
getNext中添加了部分输出 以更好理解计算next数组的过程
class Solution {
/**
* 基于kmp的算法
* <p>
* 时间复杂度:O(m + n)
* 空间复杂度:O(m)
* 注:n为haystack的长度,m为needle的长度
*/
public void getNext(int [] next, String s){
// j 指向前缀末尾位置 i 指向后缀末尾位置
// next[i] 表示 i (包括i) 之前最长相等的前后缀长度(其实就是 j)
int j = -1;
next[0] = j;
for(int i = 1; i < s.length(); i++){
System.out.println("i = " + i);
//处理前后缀不相等的情况
while(j >= 0 && s.charAt(i) != s.charAt(j + 1)){
j = next[j]; //向前回退
System.out.println("回退" + j);
}
//处理前后缀相等的情况
if(s.charAt(i) == s.charAt(j + 1)){
j++;
System.out.println("相等" + j);
}
next[i] = j;
System.out.println(next[i]);
}
}
public int strStr(String haystack, String needle) {
if(needle.length() == 0){
return 0;
}
int len = needle.length();
int [] next = new int [len];
getNext(next, needle);
int j = -1;//因为next数组里记录的初始位置为-1
for(int i = 0; i < haystack.length(); i++){//i从0开始
//不匹配
while(j >= 0 && haystack.charAt(i) != needle.charAt(j + 1)){
j = next[j];//回退,j寻找之前匹配的位置
}
//匹配,i与j同时向后移动
if(haystack.charAt(i) == needle.charAt(j + 1)){
j++;
}
//文本串里出现了模式串
if(j == needle.length() - 1){
return (i - needle.length() + 1);
}
}
return -1;
}
}