28. 找出字符串中第一个匹配项的下标
暴力求解
这一题蛮简单,但是逻辑上容易忽略一些小细节。我一开始想的是从haystack数组的头开始找与needle匹配的第一个字符,然后再顺着看下面连续的字符是否匹配,然后直接返回结果,但是忽略了很重要的一点:haystack中可能有多个字符和needle的第一个字符是一样的,那么我们不能一次匹配就返回判断结果,而是要遍历整个haystack进行判断。
class Solution {
public:
int strStr(string haystack, string needle) {
int j = 0;
int index = -1;
for(int i = 0; i < haystack.size(); i++) {
if(haystack[i] == needle[j]) { // 首先在haystack中找到needle的第一个字符
index = i; // 并记录第一个匹配字符的下标index
// 进入循环检查是否接下来每一个字符都是与needle匹配的
while(haystack[i] == needle[j] && j < needle.size() && i < haystack.size()) {
i++;
j++;
}
if(j == needle.size()) return index; // 如果needle中的字符全检查完了,则是匹配的,返回对应的index
else return -1; // 如果needle中的字符没有检查完,即不匹配,则返回-1
}
}
return index; // for循环已经结束都没有返回的话,说明haystack中没有匹配字符,则返回-1
}
};
第二次修改后的如下,但是还是错了,因为我直接用外层的for循环的变量i去做字符串匹配检查,这其中包含了i++
指令,更改了i的值,那么会影响到最外层的for循环。
class Solution {
public:
int strStr(string haystack, string needle) {
int index = -1; // index默认值是-1
for(int i = 0; i < haystack.size(); i++) {
int j = 0;
if(haystack[i] == needle[j]) { // 首先在haystack中找到与needle匹配的第一个字符
index = i; // 并记录第一个匹配字符的下标index
// 进入循环检查是否接下来每一个字符都是与needle匹配的
while(haystack[i] == needle[j] && j < needle.size() && i < haystack.size()) {
i++;
j++;
}
if(j == needle.size()) return index; // 如果needle中的字符全检查完了,则是匹配的,返回对应的index
else index = -1; // 如果needle中的字符没有检查完,即不匹配,index要恢复默认值
}
}
return index; // for循环已经结束都没有返回的话,说明haystack中没有匹配字符,则返回-1
}
};
最终正确代码如下:
class Solution {
public:
int strStr(string haystack, string needle) {
int index = -1; // index默认值是-1
for(int i = 0; i < haystack.size(); i++) {
int j = 0;
if(haystack[i] == needle[j]) { // 首先在haystack中找到与needle匹配的第一个字符
index = i; // 并记录第一个匹配字符的下标index
int k = i; // 用k来代替i进行接下来的检查(不然i值被改变会影响最外层的for循环)
// 进入循环检查是否接下来每一个字符都是与needle匹配的
while(haystack[k] == needle[j] && j < needle.size() && k < haystack.size()) {
k++;
j++;
}
if(j == needle.size()) return index; // 如果needle中的字符全检查完了,则是匹配的,返回对应的index
else index = -1; // 如果needle中的字符没有检查完,即不匹配,index要恢复默认值
}
}
return index; // for循环已经结束都没有返回的话,说明haystack中没有匹配字符,则返回-1
}
};
KMP算法
上面的暴力求解算法不难看出时间复杂度是O(n × m),而KMP算法可以将效率提高至O(n + m)。那么什么是KMP算法呢?
他的核心思想在于定义一个模式串的前缀表,然后在目标串中依次比对,碰到不匹配的地方,则回退到模式串此处的相同前缀处继续比对,就不必再退回到模式串的开始而从头再来。文字可能比较难以理解,大家可以去看代码随想录中的示例图,非常清晰明了。
首先我们构造模式串的前缀表,注意这里构造的形式不是固定的,有的前缀表初始值为0,有的为-1(整体向右移),看需求与习惯来决定如何构造,但是核心方法是一样的:
- 初始值为0
void getNext(int* next, string& s) {
int j = 0;
next[0] = j; // next初始值为0
for(int i = 1; i < s.size(); i++) {
while(j >= 1 && s[i]!=s[j]) // 如果当前前后缀不同,j就要退回到与当前s[i]相等的前缀上
j = next[j-1];
if(s[i]==s[j]) j++; // 如果当前前后缀相同,则j向前一位,即指到j+1上
next[i] = j; // 将当前前缀的长度赋予next[i]
}
}
- 初始值为-1
void getNext(int* next, string& s) {
int j = -1;
next[0] = j; // next初始值为-1
for(int i = 1; i < s.size(); i++) {
while(j >= 0 && s[i]!=s[j+1]) // 如果当前前后缀不同,j就要退回到与当前s[i]相等的前缀上
j = next[j];
if(s[i]==s[j+1]) j++; // 如果当前前后缀相同,则j向前一位,即指到j+1上
next[i] = j; // 将当前前缀的长度赋予next[i]
}
}
然后再利用next数组来匹配。对于初始值为0的前缀表,我们碰到不匹配的地方是跳到next[j-1]
处匹配,而初始值为-1的前缀表,我们是跳到next[j]
处。
- 初始值为0
int strStr(string haystack, string needle) {
int* next = new int[needle.size()]; // 初始化next数组
getNext(next, needle);
int j = 0; // next数组起始位置是0
for(int i = 0; i < haystack.size(); i++) {
while(j >= 1 && haystack[i] != needle[j]) { // 如果匹配到不相等的,j就要跳回到相等前缀处,即next[j-1]
j = next[j-1];
}
if(haystack[i] == needle[j]) {
j++;
}
if(j == (needle.size())) {
delete[] next;
return (i-needle.size()+1);
}
}
delete[] next;
return -1;
- 初始值为-1
int strStr(string haystack, string needle) {
int* next = new int[needle.size()]; // 初始化next数组
getNext(next, needle);
int j = 0; // next数组起始位置是-1
for(int i = 0; i < haystack.size(); i++) {
while(j >= 1 && haystack[i] != needle[j+1]) { // 如果匹配到不相等的,j就要跳回到相等前缀处,即next[j]
j = next[j];
}
if(haystack[i] == needle[j+1]) {
j++;
}
if(j == (needle.size() - 1)) {
delete[] next;
return (i-needle.size()+1);
}
}
delete[] next;
return -1;
459.重复的子字符串
暴力求解
class Solution {
public:
bool repeatedSubstringPattern(string s) {
int fast = 1;
while(fast < s.size()) { // 遍历快指针
if(s[fast] == s[0]) { // 找到与s[0]相同的字符
int i = 0;
int j = fast;
while(s[i] == s[j] && j < s.size()) { // 检查他们对应位置的字符是否一样
i++;
j++;
}
if(j == s.size() && i % fast == 0) return true; // 如果检查到字符串末尾都是一样的,则是重复子串,返回true
}
fast++; // // 如果没有检查到末尾,则还没有找到子串,fast继续向后找
}
return false;
}
};
KMP算法
假设重复子串的长度为 x x x,那么整个字符串的长度为 n × x n\times x n×x,最长相等前后缀长度则为 m × x m\times x m×x,而字符串减去最长相等前后缀即是重复子串: m x − n x = x mx-nx=x mx−nx=x。所以有: m x % ( m x − n x ) = 0 mx \% (mx-nx) = 0 mx%(mx−nx)=0。
class Solution {
public:
void getNext(int* next, string& s) {
int j = -1;
next[0] = j; // 首先对next数组赋初值:s的第一个字符必定是没有相等前后缀的
for(int i = 1; i < s.size(); i++) { // 从s的第二个字符开始看
while(j >= 0 && s[i]!=s[j+1]) { // 利用while循环找到与s[i]相等的前缀
j = next[j];
}
if(s[i]==s[j+1]) j++; // 如果找到了相等前缀,就要让j补1(因为此处的前缀表是统一减1的)
next[i] = j;
}
}
bool repeatedSubstringPattern(string s) {
int len = s.size();
if (!len) return false; // 如果s本身就是空串,则直接返回false
int next[len]; // 构造前缀表
getNext(next, s);
if (next[len-1]!=-1 && len % (len - (next[len-1] + 1)) == 0) {
return true;
}
return false;
}
};