目录
KMP算法思想
KMP算法和暴力算法对比
KMP算法主要解决字符串匹配的问题。
当需要在字符串中查找子串,常用的两种方法分别是暴力解法和KMP算法。
暴力解法是使用两层 for 循环依次遍历对比两个字符串,一个指针 i 遍历字符串,一个指针 j 遍历子串,当子串中遍历到 j 位置的字符不匹配,我们需要让 j 从头开始遍历,同时 i 也要回退,故暴力解法的时间复杂度是 O(mxn),其中 m 是字符串的长度,n 是子串的长度。
而 KMP 算法是先求出前缀表,让子串遍历到 j 位置发现不匹配时可以不用从头遍历,而是查询前缀表让 j 跳转回子串的某个中间位置再次匹配,同时也可以让字符串的指针 i 继续向前遍历,无需回退,时间复杂度是 O(m)。
如何找到子串之前已经匹配过的内容?
前缀表
我们可以使用前缀表(即 next 数组)存储 每当子串中的该字符不匹配时 需要跳转到哪个下标继续匹配,这样我们就不用每次都从头开始遍历,而是从子串的某个中间位置继续遍历。
Q1:什么是前缀?
在字符串中只包含首字母、不包含尾字母的子串都是前缀,例如字符串 “aabaaf”,前缀有“a”、“aa”、“aab”、“aaba”、“aabaa”,但是没有“aabaaf”。
Q2:什么是后缀?
在字符串中只包含尾字母、不包含首字母的子串都是后缀,例如字符串 “aabaaf”,后缀有“abaaf”、“baaf”、“aaf”、“af”、“f”,但是没有“aabaaf”。
Q3:如何找到该跳转到哪个下标呢?也就是如何求前缀表呢?
设子串为 string subStr,我们需要找到当 subStr[ j ] 不匹配时,前 (j - 1) 个字符构成的字符串中是否有相同的前缀、后缀,如果有,这前缀或后缀的最大长度又是多少?
求前缀表的代码实现
对于KMP算法思想已经理解,但是代码实现还有些步骤无法搞懂,因此等后续多理解后再进行更新。
反转字符串的单词
题干
题目:给定一个字符串s,逐个翻转字符串中的每个单词。注意:单词之间可能不止一个空格, 字符串 s 前也可能有多个空格。s 包含英文大小写字母、数字和空格。
示例 1: 输入: "the sky is blue" 输出: "blue is sky the"
示例 2: 输入: " hello world! " 输出: "world! hello" 解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
示例 3: 输入: "a good example" 输出: "example good a" 解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
思路
方法一使用额外空间:从前往后遍历原字符串,用字符串 word 记录单词,当遍历到单词,插入新字符串中且是从头部插入;当遍历到空格,要做两个判断,一是这个空格是否在句子的最开始或最末尾,这些空格都不需要插入新字符串中,二是要判断是否有重复的空格,如果重复则一直遍历到最后一个空格才插入到新字符串。新字符串 newStr 存储的便是反转后的结果。时间复杂度 O(n),空间复杂度O(n)。
方法二原地反转:先移除字符串中多余的空格,再将整个字符串反转,此时单词也跟着反转了,那么最后再只把单词反转回正确的顺序即可(也就是反转两次就行)。清除空格有两种方法,一种是使用erase,一种是用双指针法。而erase 操作本身时间复杂度就为O(n),在循环遍历字符串时再 erase 那么时间复杂度就是 O(n^2)。只是力扣的测试集字符串不够长所以看起来和双指针法的性能差不多,但是当字符串长度很大时性能差异会很明显。双指针法即快慢指针法,在之前学习数组章节时做过 “移除数组内指定元素” 的题目,原理相同,时间复杂度为O(n),在这里不过多赘述。
代码
方法一:使用额外空间
class Solution {
public:
string reverseWords(string s) {
string newStr; // 记录反转后的结果
string word; // 记录单词
for (int i = 0; i < s.size(); ++i) {
word.clear(); // 每次都要初始化单词
if (s[i] == ' '){
// 对空格去重,先遍历到最后一个空格处
while (s[i+1] == ' '){
i++;
}
if (i != s.size()-1 && newStr.size() != 0){
// i != s.size()-1 是为了保证句子末尾的空格不用插入新字符串
// newStr.size() != 0 是为了保证句子开头的空格不用插入新字符串
newStr.insert(0," ");
}
} else{
while (i < s.size() && s[i+1] != ' '){
word.push_back(s[i]);
i++; // 此时 i 遍历到最后可能会越界,所以要 i < s.size()
}
if (i < s.size()){
word.push_back(s[i]);
}
newStr.insert(0,word); // 头插法就是逆序插入单词
}
}
return newStr;
}
};
方法二:原地反转,用 erase 去除空格
class Solution {
public:
string reverseWords(string s) {
// 双指针法反转字符串
int left = 0;
int right = s.size()-1;
while (left < right){
if (s[left] == ' '){
while (s[left+1] == ' '){
// 要注意删除元素后 right 下标会减一,left 下标不影响
s.erase(left,1);
right--;
}
if (left == 0){
s.erase(left,1);
right--;
}
}
if (s[right] == ' '){
while (s[right-1] == ' '){
s.erase(right,1);
right--;
}
if (right == s.size()-1){
s.erase(right,1);
right--;
}
}
swap(s[left],s[right]);
left++;
right--;
}
left = 0;
right = 0;
// 再反转单词,此时 left 和 right 分别指向单词的起始位置和终止位置
for (int i = 0; i < s.size(); ++i) {
if (s[i] != ' '){
left = i;
// 注意数组越界,不然会死循环
while (i < s.size()-1 && s[i+1] != ' '){
i++;
}
right = i;
while (left < right){
swap(s[left],s[right]);
left++;
right--;
}
}
}
return s;
}
};
方法三:原地反转,双指针法去除空格
class Solution {
public:
// 快慢指针法移除字符串中的空格,参考之前做的一道题 “移除数组中的指定元素”
string removeSpaces(string s){
int slow = 0;
int fast;
int oldLen = s.size();
for (fast = 0; fast < oldLen; ++fast) {
if (s[fast] == ' '){
while (s[fast+1] == ' '){
fast++;
}
if (slow != 0 && fast != oldLen-1){
s[slow] = s[fast];
slow++;
}
} else{
s[slow] = s[fast];
slow++;
}
}
s.resize(slow);
return s;
}
string reverseWords(string s) {
s = removeSpaces(s); // 移除多余的空格
// 在上个方法中已经用过自定义 reverse 方法了,所以这里就尝试着换成 C++ 自带的reverse函数
reverse(s.begin(),s.end()); // 反转整个字符串
// 只反转单词
int left = 0; // 记录单词的起始位置
int right = 0; // 记录单词的终止位置
for (int i = 0; i < s.size(); ++i) {
if (s[i] != ' '){
left = i;
while (i < s.size()-1 && s[i+1] != ' '){
i++;
}
right = i;
reverse(s.begin()+left, s.begin()+right+1); // 标准库中的reverse函数是左闭右开,和自定义的不同
}
}
return s;
}
};
右旋字符串
题干
题目:字符串的右旋转操作是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,请编写一个函数,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。
例如,对于输入字符串 "abcdefg" 和整数 2,函数应该将其转换为 "fgabcde"。
输入:输入共包含两行,第一行为一个正整数 k,代表右旋转的位数。第二行为字符串 s,代表需要旋转的字符串。
输出:输出共一行,即进行了右旋转操作后的字符串。
思路
方法一使用库函数:将末尾 k 个字符的子串插入到原字符串的前面,再调整字符串的长度为原始长度,这样就可以把末尾的 k 个字符删除。
方法二原地右旋:先将原字符串反转一次,如"abcdefg" (k = 2),反转后变成“gfedcba”,此时后 k 个字符就移动到了前面,但字母顺序反了,只需将第 0 ~ k-1 个字符再反转,将第 k ~ 末尾 个字符再反转,就能获得右旋后的字符串(总共反转三次)。
总结:常用思想是先整体反转字符串,再进行局部反转。左旋操作同理。
代码
方法一:使用库函数
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main(){
int k; // 右旋转的位数
scanf("%d",&k);
getchar();// 读取换行
string s; // 要旋转的字符串
getline(cin,s);
int oldLen = s.size(); // 记录原始的字符串长度
string subStr = s.substr(s.size()-k); // 提取后 k 个字符的子串
s.insert(0,subStr); // 将子串插入在开头
s.resize(oldLen);
printf("%s",s.c_str());
return 0;
}
方法二:原地右旋
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
string reverse(string s, int left, int right){
for (left,right; left < right; ++left,--right) {
swap(s[left],s[right]);
}
return s;
}
int main(){
int k; // 右旋转的位数
scanf("%d",&k);
getchar();// 读取换行
string s; // 要旋转的字符串
getline(cin,s);
s = reverse(s,0,s.size()-1); // 左闭右闭
s = reverse(s,0,k-1);
s = reverse(s,k,s.size()-1);
printf("%s",s.c_str());
return 0;
}
拓展:左旋
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
string reverse(string s, int left, int right){
for (left,right; left < right; ++left,--right) {
swap(s[left],s[right]);
}
return s;
}
int main(){
int k; // 右旋转的位数
scanf("%d",&k);
getchar();// 读取换行
string s; // 要旋转的字符串
getline(cin,s);
s = reverse(s,0,s.size()-1); // 左闭右闭,先反转整个字符串
s = reverse(s,0,s.size()-k-1);
s = reverse(s,s.size()-k,s.size()-1);
printf("%s",s.c_str());
return 0;
}