目录
一、LeetCode 151.翻转字符串里的单词
题目链接:151.翻转字符串里的单词
文章讲解:代码随想录
视频讲解:字符串复杂操作拿捏了! | LeetCode:151.翻转字符串里的单词
思路:
1. 栈操作
笔者对于本题的思路是引入C++中的<stack>
容器类,即建立一个栈,借助栈数据结构“后入先出”
(
L
I
F
O
)
(LIFO)
(LIFO) 的特性,实现单词顺序的反转,并且当遇到空格时不操作可以有效规避多余空格;
具体思路为,建立单词栈strStack
,使用快慢指针遍历字符串s
,当前读取字符不为" "
时,fast
指针向后延伸,slow
保持在单词开头位置,直到fast
探测到单词末尾,将单词压入栈,继续循环操作;
待所有单词压入栈后,对原字符串进行清空s.clear()
,之后遍历单词栈,将栈中单词写入原字符串s
,得到的新字符串即为反转单词顺序后的字符串。
该方法好处在于处理空格与反转较方便,时间复杂度为 O ( n ) O(n) O(n) ,但建立单词栈与中间变量的空间复杂度会达到 O ( n ) O(n) O(n) ,可以使用双指针方法将空间复杂度降到 O ( 1 ) O(1) O(1).
2. 双指针+两次反转
双指针的作用主要在于去除原字符串中的空格,而后使用两次 344. 反转字符串 的方法,先反转整个字符串,再反转字符串中每个单词,即可实现单词顺序的反转。
举个例子,源字符串为:"the sky is blue "
- 移除多余空格 : “the sky is blue”
- 字符串反转:“eulb si yks eht”
- 单词反转:“blue is sky the”
C++代码
栈操作
class Solution {
public:
string reverseWords(string s) {
stack<string> strStack; //引入栈
int slow = 0, fast = 0;
while(fast < s.size()){
while(s[slow] == ' '){
slow++;
}
fast = slow; //存储单词开头位置
while(fast < s.size() && s[fast] != ' '){ //探测单词结尾位置
fast++;
}
string tmp = s.substr(slow, fast-slow); //截取单词
tmp.push_back(' '); //手动给单词加空格
strStack.push(tmp); //单词入栈
fast++;
slow = fast;
}
s.clear(); //清空字符串,以待重新写入
while(!strStack.empty()){ //写入操作
string tmp = strStack.top();
strStack.pop();
if(tmp != " "){
s.append(tmp);
}
}
s.pop_back(); //因为栈中每个单词后面都加了空格,最终需要去掉结尾一个空格
return s;
}
};
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
n
)
O(n)
O(n)
双指针+两次反转
class Solution {
public:
void reverse(string& s, int start, int end){ //翻转,区间写法:左闭右闭 []
for (int i = start, j = end; i < j; i++, j--) {
swap(s[i], s[j]);
}
}
void removeExtraSpaces(string& s) {//去除所有空格并在相邻单词之间添加空格, 快慢指针。
int slow = 0; //整体思想参考https://programmercarl.com/0027.移除元素.html
for (int i = 0; i < s.size(); ++i) { //
if (s[i] != ' ') { //遇到非空格就处理,即删除所有空格。
if (slow != 0) s[slow++] = ' '; //手动控制空格,给单词之间添加空格。slow != 0说明不是第一个单词,需要在单词前添加空格。
while (i < s.size() && s[i] != ' ') { //补上该单词,遇到空格说明单词结束。
s[slow++] = s[i++];
}
}
}
s.resize(slow); //slow的大小即为去除多余空格后的大小。
}
string reverseWords(string s) {
removeExtraSpaces(s); //去除多余空格,保证单词之间之只有一个空格,且字符串首尾没空格。
reverse(s, 0, s.size() - 1);
int start = 0; //removeExtraSpaces后保证第一个单词的开始下标一定是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;
}
};
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
1
)
O(1)
O(1)
二、卡码网:右旋字符串
题目链接:右旋字符串
文章讲解:代码随想录
思路
本题仍然有两种思路,参考上一题反转字符串中单词顺序中两次反转的思路,把右旋过程当成没有空格的单词顺序反转也可;
另一方面,因为右旋若看做是反转单词顺序,那么只涉及两个单词的反转,那么这时字符串基本操作s.insert()
与s.erase()
在性能上就不会显得底下了,可以直接使用。
C++代码
两次反转
// 版本一
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int n;
string s;
cin >> n;
cin >> s;
int len = s.size(); //获取长度
reverse(s.begin(), s.end()); // 整体反转
reverse(s.begin(), s.begin() + n); // 先反转前一段,长度n
reverse(s.begin() + n, s.end()); // 再反转后一段
cout << s << endl;
}
字符串类函数操作
#include <iostream>
#include <string>
using namespace std;
class Solution{
public:
string StrDex(string s, int k){
string tmp = s.substr(s.size()-k, k);
s.erase(s.size()-k, k);
s.insert(0, tmp);
return s;
}
};
int main(){
Solution sol;
string s;
int k;
cin >> k >> s;
s = sol.StrDex(s, k);
cout << s;
return 0;
}
三、LeetCode 28. 实现 strStr()
题目链接:28. 实现 strStr()
文章讲解:代码随想录
视频讲解:帮你把KMP算法学个通透!(理论篇)
帮你把KMP算法学个通透!(求next数组代码篇)
思路
本题要求实现字符串匹配的操作,实际上可以直接使用C++<string>
类函数s.find()
实现:
class Solution {
public:
int strStr(string haystack, string needle) {
return haystack.find(needle);
}
};
然而这题意在答题者使用自己编写的算法,且在字符串匹配类题目上,
K
n
u
t
h
−
M
o
r
r
i
s
−
P
r
a
t
t
Knuth-Morris-Pratt
Knuth−Morris−Pratt 算法、
B
o
y
e
r
−
M
o
o
r
e
Boyer-Moore
Boyer−Moore 算法、
S
u
n
d
a
y
Sunday
Sunday 算法是绕不开的算法,因此可以借助本题复习 学习
K
M
P
KMP
KMP 算法.
K M P KMP KMP 算法的理论与实现部分较长,因此笔者单独开一篇文章记录本题使用 K M P KMP KMP 算法的情形,具体讲解跳转:头秃复习: K M P KMP KMP 算法解 LeetCode 28. 实现 strStr().
四、LeetCode 459.重复的子字符串
题目链接:459.重复的子字符串
文章讲解:代码随想录
视频讲解:字符串这么玩,可有点难度! | LeetCode:459.重复的子字符串
思路
题目要求判断给出的字符串是否是一个子串循环生成的,在具体实现上会涉及子串匹配的问题,即我们上一题提到的 K M P KMP KMP 算法中的一些内容,但这题标的简单是否搞错了什么…
1. 前缀表
笔者使用的方法是借助
K
M
P
KMP
KMP 算法中构建的前缀表next
数组进行判断;
笔者对于前缀表一开始设计的算法为:找到前缀表第一个位置,使得next[i] == i+1
,因为满足子串重复的字符串会在子串出现第二次时,第二个子串末尾的前缀表值就是子串长度,例如:
上图字符串中,循环生成的子串为
a
b
c
abc
abc ,当
a
b
c
abc
abc 第二次出现时,第二个子串末尾的
c
c
c 的值恰好就是子串长度
3
3
3;笔者通过这种方式编写算法时发现存在缺陷,如字符串
a
b
a
a
b
a
c
a
b
a
a
b
a
c
abaabacabaabac
abaabacabaabac:
如图可见:该算法在识别子字符串中有部分重复的情形会识别出错,因此这种算法不可行
然而在笔者某次调试程序时,看到了内存中
n
e
x
t
next
next 数组的值(字符串为
a
b
a
c
a
b
a
c
a
b
a
c
abacabacabac
abacabacabac):
可以看到,满足循环生成的字符串,从第二次循环开始的前缀表值为自然数递增序列,那么可以根据这一特性设计算法;
从上图中我们可以看出重复子字符串的几种特性:
- 从第二遍重复开始,前缀表
next
的值为从 1 1 1 开始,步长为 1 1 1 的正整数序列 - 字符串结束位置的
next[i]
值为字符串长度与子串长度的差,即next[s.size()-1] == s.size()-substr.size()
我们可以根据第二条特性构建一个判别标准:
构建好
next
数组以后,取next
最后一个元素,与原字符串的长度s.size()
作差得到diff
,若该字符串为子串的重复,那么字符串长度必然能被diff
整除.
这个判别标准可以通过数学推导简单证明:
假设字符串 s s s 使用多个重复子串构成(这个子串是最小重复单位),重复出现的子字符串长度是 x x x ,所以 s s s 是由 n ∗ x n * x n∗x 组成。
因为字符串 s s s 的最长相同前后缀的长度一定是不包含 s s s 本身,所以 最长相同前后缀长度必然是 m ∗ x m * x m∗x,而且 n − m = 1 n - m = 1 n−m=1
所以如果 n x % ( n − m ) x = 0 nx \% (n - m)x = 0 nx%(n−m)x=0,就可以判定有重复出现的子字符串。
因此我们的算法分为两个部分:构建next
数组与利用判别标准求解
构建next
数组的方法在 头秃复习:
K
M
P
KMP
KMP 算法解 LeetCode 28. 实现 strStr() 中有详细讲述,在此不再赘述;构建好next
数组后使用nx % (n - m)x = 0
判断即可,其中
n
x
nx
nx 为原字符串长度,
m
m
m 为next
数组中最后一个元素.
C++代码
class Solution {
public:
bool repeatedSubstringPattern(string s) {
if(s.size() == 1){
return false;
}
vector<int> next(s.size());
next[0] = 0;
int j = 0;
//构建next数组
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 diff = next.size() - j;
if(j > 0 && next.size()%diff == 0){ //若可被子串长度整除,则满足条件
return true;
}
else{
return false;
}
}
};
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
n
)
O(n)
O(n)
2. 移动匹配
移动匹配利用了字符串中重复的特性,即一个重复的子字符串组成的字符串中,前后部分一样
那么当两个这样的字符串拼起来时,一定能在新字符串中间匹配到一个原字符串:
因此,只需要排除s+s
的首尾子字符串,通过匹配就可以确定该字符串是不是一个子字符串的重复,代码如下:
C++代码
class Solution {
public:
bool repeatedSubstringPattern(string s) {
string t = s + s;
t.erase(t.begin()); t.erase(t.end() - 1); // 掐头去尾
if (t.find(s) != std::string::npos) return true; // r
return false;
}
};
该算法中使用类函数erase()
,find()
的时间复杂度实际上会达到
O
(
m
+
n
)
O(m+n)
O(m+n),因此在性能上较前缀表的算法差一些。
总结
对字符串的基本操作逐渐熟悉,字符串的进阶题目趋向于自己修改或设计算法解决特定问题,并且需要注意find()
与erase()
类函数本身的时间复杂度(
O
(
n
)
O(n)
O(n))。字符串部分需要强调对
K
M
P
KMP
KMP 算法复习。
文章部分图片来源:代码随想录 (https://programmercarl.com/)