151.翻转字符串里的单词
最简单的是使用split库函数,分隔单词,然后定义一个新的string字符串,最后再把单词倒序相加。进阶做法是分为三步:
- 移除多余空格
- 将整个字符串反转
- 将每个单词反转
举个例子,源字符串为:"the sky is blue "
- 移除多余空格 : "the sky is blue"
- 字符串反转:"eulb si yks eht"
- 单词反转:"blue is sky the"
在移除多余空格时,如果采用erase方法,由于erase方法本身的复杂度是O(n),考虑到外层的for循环,只是移除多余空格,时间复杂度就是O(n^2)
void removeExtraSpaces(string& s) {
for (int i = s.size() - 1; i > 0; i--) {
if (s[i] == s[i - 1] && s[i] == ' ') {
s.erase(s.begin() + i);
}
}
// 删除字符串最后面的空格
if (s.size() > 0 && s[s.size() - 1] == ' ') {
s.erase(s.begin() + s.size() - 1);
}
// 删除字符串最前面的空格
if (s.size() > 0 && s[0] == ' ') {
s.erase(s.begin());
}
}
注意到移除空格和题27.移除元素很像,只是这里移除的元素是空格。但又有所不同,因为首尾的空格要全部删除,而单词间的空格要保留一个,所以要分开处理。思路一就是按照上述思路修改题27的算法,思路二是先去除所有空格,但循环中在非第一个单词的其它单词前手动添加空格,思路二写出来的代码更简洁
// 思路一
void removeExtraSpace(string& s) {
int slow_index = 0, fast_index = 0;
// 去除字符串前边所有空格,也就是将fast_index移动到第一个非空格字符
while(s.size() > 0 && fast_index < s.size() && s[fast_index] == ' ') fast_index++;
// 去除单词之间多余的空格
for (;fast_index < s.size(); fast_index++) {
// 跳过多余的空格
if (fast_index > 0 // 确保fast_index-1 >= 0
&& s[fast_index] == s[fast_index - 1]
&& s[fast_index] == ' ') {
continue;
}
s[slow_index++] = s[fast_index]; // 复制其它字符
}
// 第二步结束后,slow_index-1的位置可能存在一个空格
if(slow_index > 0 && s[slow_index - 1] == ' ') {
s.resize(slow_index - 1);
} else {
s.resize(slow_index);
}
}
// 思路二
void removeExtraSpace(string& s) {
int slow_index = 0;
for (int fast_index = 0; fast_index < s.size(); fast_index++) {
// 只处理不等于空格的字符
if (s[fast_index] != ' ') {
if (slow_index != 0) s[slow_index++]= ' '; // 除第一个单词,其余单词前手动加一个空格
while(fast_index < s.size() && s[fast_index] != ' ') {
s[slow_index++] = s[fast_index++]; // 循环复制字符,直到遇到下一个空格
}
}
}
s.resize(slow_index);
}
翻转整个字符串和翻转单个单词的函数在题344.翻转字符串
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
void removeExtraSpace(string& s) {
int slow_index = 0;
for (int fast_index = 0; fast_index < s.size(); fast_index++) {
// 只关注不为空格的字符
if (s[fast_index] != ' ') {
if (slow_index != 0) s[slow_index++] = ' '; // 除第一个单词,其余单词前手动加空格
while (fast_index < s.size() && s[fast_index] != ' ') {
s[slow_index++] = s[fast_index++]; // 循环复制字符,直到遇到下一个空格
}
}
}
s.resize(slow_index);
}
void reverse(string&s, int start, int end) { //翻转,区间写法:左闭右闭 []
for (int i = start, j = end; i < j; i++, j--) {
swap(s[i], s[j]);
}
}
string reverseWords(string s) {
// 1. 去除多余空格
removeExtraSpace(s);
// 2. 翻转整个字符串
reverse(s, 0, s.size() - 1);
// 3. 翻转单个单词
int start = 0; // 单词第一个字母索引
for (int i = 0; i <= s.size(); i++) { // <=是因此要循环到串尾后一个位置,才知道最后一个单词结束
if (i == s.size() || s[i] == ' ') { //到达空格或者串尾,说明一个单词结束。进行翻转。
reverse(s, start, i - 1); //翻转,注意是左闭右闭 []的翻转
start = i + 1; //更新下一个单词的开始下标start
}
}
return s;
}
};
55.右旋字符串
可以在本串上直接操作,具体来说,就是先翻转整串,再将两个字串翻转,负负得正,就得到题目要求的结果了
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main() {
int n;
string s;
cin >> n;
cin >> s;
reverse(s.begin(), s.end()); // 翻转整个字符串
reverse(s.begin(), s.begin() + n); // 翻转前一段,长度n
reverse(s.begin() + n, s.end()); // 翻转后一段,长度为s.size() - n
cout << s << endl;
}
也可以先局部翻转,再整体翻转,只是注意第一段的长度就变成了s.size() - n
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main() {
int n;
string s;
cin >> n;
cin >> s;
reverse(s.begin(), s.begin() + s.size() - n); // 翻转前一段,长度length - n;
reverse(s.begin() + s.size() - n, s.end()); // 翻转后一段,长度为n
reverse(s.begin(), s.end()); // 翻转整个字符串
cout << s << endl;
}
28. 找出字符串中第一个匹配项的下标
本题其实就是KMP算法所要解决的主要问题,即字符串匹配问题
最直接的办法就是两层for循环,外层遍历主字符串,用于确定匹配开始的位置。内层从此位置比较主字符串与模式字符串每个字符是否相同,完全相同就停止匹配,返回外层循环变量,否则继续外层循环。
KMP算法的精髓:回想上述暴力算法,内层循环时,当遇到有字符不相同就break,然后外层循环将开始匹配的位置后移一位,这是必须的么?假设模式字符串里,在冲突发生位置之前的部分是abcde,这部分确认是可以和主字符串相应位置匹配的,于是可以确定主字符串这部分内容也是abcde,那么将匹配开始位置后移一位,前边的部分就是要确认主字符串中的bcde和模式字符串abcd是否相同,也就是要确认模式字符串里的abcd和bcde是否相同。如果不同,那自然又要将匹配开始位置后移一位,前边的部分要确认主字符串里的cde和模式字符串里的abc是否相同。每次要确认的部分就是KMP算法所谓的前缀和后缀,以上过程在每次冲突发生时,都会发生。
- 前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。
- 后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。
那么,KMP算法的思想就是,将这个重复的过程利用起来,提前计算出来若当前字符冲突了,将开始匹配位置右移,也就是模式字符串右移几位,才可以确保移动之后,在冲突位置之前的部分是可以匹配的上。这样不用一次次右移,就加快了匹配的速度。
这里的右移几次,又叫做最大相等前后缀长度,
将每个模式字符串的位置冲突时的,最大相等前后缀长度计算出来,就得到了前缀表。
因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。
所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。
来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:
时间复杂度分析:n为文本串长度,m为模式串长度,从上图可以看出匹配的过程是O(n),由于要额外生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大地提高了搜索的效率。
那么,前缀表怎么计算呢,这其实还挺复杂的,具体查看代码随想录相应内容代码随想录 (programmercarl.com)
前缀表与next数组:很多KMP算法的实现都是使用next数组来做回退操作,next数组可以是前缀表,但是很多实现都是把前缀表统一减一(或右移一位,初始位置为-1)之后作为next数组。其实这并不涉及到KMP的原理,只是具体实现。以下我们以前缀表统一减一之后的next数组来做演示
构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步:
- 初始化
- 处理前后缀不相同的情况
- 处理前后缀相同的情况
// 前缀表所有值减一,对应的next表求法
void getNext(int* next, const string& s){
int j = -1; // j初始化为-1
next[0] = j; // 第一个位置值恒为-1
// i指向后缀末尾,j+1指向前缀末尾
// 比如aabaaf, 一种情况是,前缀为aab, j+1指向b, 后缀为aaf, i指向f
// 前缀后缀长度相同
// i从1开始,因为第一个有效后缀索引就是1
for(int i = 1; i < s.size(); i++) {
// 新的前后缀末尾不同
while (j >= 0 && s[i] != s[j + 1]) { // j=-1时就不需要回退了,跳出循环
// 比如aabaaf, 前缀为aab, 后缀为aaf, 末尾一个为b, 一个为f,不相同
// 于是寻找比较aa与af, 再比较a与f
// 可以看到上述过程类似于以后缀aaf为主串,以前缀aab为模式串进行的匹配
// 这里用KMP的思想,前缀向右移动几位,才能保证移动后后缀f之前的字符能匹配上
// 那不就是f前一位的最大相等前后缀么,也就是next[j]的值
// 这在之前已经求出来了
j = next[j]; // 向前回退
}
// 新的前后缀末尾相同,最大相等前后缀长度自然加1
if (s[i] == s[j + 1]) {
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
寻找模式串第一个匹配位置的整体代码如下
class Solution {
public:
void getNext(int* next, const string& s) {
int j = -1;
next[0] = j;
for (int i = 1; i < s.size(); i++) {
// 新的前后缀末尾不同了
while (j >= 0 && s[i] != s[j + 1]) {
j = next[j]; // 回退
}
// 新的前后缀末尾相同
if (s[i] == s[j + 1]) {
j++;
}
// 将j(前缀的长度)赋给next[i]
next[i] = j;
}
}
int strStr(string haystack, string needle) {
// 如果needle为空返回0
if (needle.size() == 0) {
return 0;
}
vector<int> next(needle.size());
getNext(&next[0], needle);
// 将主字符串和模式字符串,看作next数组构造时的后缀和前缀
int j = -1;
for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始,因为这里毕竟不是后缀,后缀在needdle的第一个位置是1,而主字符串第一个位置是0
// 不匹配
while (j >= 0 && haystack[i] != needle[j + 1]) {
j = next[j]; // j寻找之前匹配的位置
}
// 匹配,j和i同时向后移动
if (haystack[i] == needle[j + 1]) {
j++; // i的增加放在for循环里
}
// j指针遍历到了模式串末尾,说明匹配成功,注意j起始值是-1不是0
if (j == needle.size() - 1) {
return (i - needle.size() + 1); // +1的原因是这里i的增加在for循环里,此时i还没有加1,要手动修改
}
}
return -1; // 没找到,返回-1
}
};
前缀表不减一的写法也是类似的
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;
}
vector<int> next(needle.size());
getNext(&next[0], 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;
}
};