一、字符串的反转
1、可以使用C++的库函数reverse
reverse函数功能是逆序(或反转),多用于字符串、数组、容器。头文件是#include
reverse函数用于反转在[first,last)范围内的顺序(包括first指向的元素,不包括last指向的元素),reverse函数无返回值
eg.
string str="hello world , hi";
reverse(str.begin(),str.end());//str结果为 ih , dlrow olleh
vector<int> v = {5,4,3,2,1};
reverse(v.begin(),v.end());//容器v的值变为1,2,3,4,5
2、
344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
示例 1:
输入:[“h”,“e”,“l”,“l”,“o”]
输出:[“o”,“l”,“l”,“e”,“h”]
示例 2:
输入:[“H”,“a”,“n”,“n”,“a”,“h”]
输出:[“h”,“a”,“n”,“n”,“a”,“H”]
链接:https://leetcode-cn.com/problems/reverse-string
思路:
双指针的方法。对于字符串,我们定义两个指针(也可以说是索引下表),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。
代码:
class Solution {
public:
void reverseString(vector<char>& s) {
// reverse(s.begin(),s.end());
int i=0,j=s.size()-1;
for(;i<=j;i++,j--){
swap(s[i],s[j]);
}
}
};
例题二(反转进阶)
- 反转字符串 II
给定一个字符串 s 和一个整数 k,你需要对从字符串开头算起的每隔 2k 个字符的前 k 个字符进行反转。
如果剩余字符少于 k 个,则将剩余字符全部反转。
如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
示例:
输入: s = “abcdefg”, k = 2
输出: “bacdfeg”
提示:
该字符串只包含小写英文字母。
给定字符串的长度和 k 在 [1, 10000] 范围内。
链接:https://leetcode-cn.com/problems/reverse-string-ii
思路:
这道题目其实也是模拟,实现题目中规定的反转规则就可以了。
一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。
其实在遍历字符串的过程中,只要让 i += (2 * k ),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。
因为要找的也就是每2 * k 区间的起点,这样写程序会高效很多。
「所以当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。」
代码:
class Solution {
public:
string reverseStr(string s, int k) {
for(int i=0;i<s.size();i+=(2*k)){
// 1. 每隔 2k 个字符的前 k 个字符进行反转
// 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符
if(i+k<=s.size()){
reverse(s.begin()+i,s.begin()+i+k);
continue;
}
// 3. 剩余字符少于 k 个,则将剩余字符全部反转。
reverse(s.begin()+i,s.end());
}
return s;
}
};
例题三
(技巧:遇到对字符串或者数组做填充或删除的操作时,都要想想从后向前操作怎么样。)
剑指 Offer 05. 替换空格
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
示例 1:
输入:s = “We are happy.”
输出:“We%20are%20happy.”
限制:
0 <= s 的长度 <= 10000
链接:https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof
思路:
如果想把这道题目做到极致,就不要只用额外的辅助空间了!
首先扩充数组到每个空格替换成"%20"之后的大小。
然后从后向前替换空格,也就是双指针法,过程如下:
i指向新长度的末尾,j指向旧长度的末尾。
有同学问了,为什么要从后向前填充,从前向后填充不行么?
从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。
「其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。」
这么做有两个好处:
- 不用申请新数组。
- 从后向前填充元素,避免了从前先后填充元素要来的 每次添加元素都要将添加元素之后的所有元素向后移动。
class Solution {
public:
string replaceSpace(string s) {
int count=0;// 统计空格的个数
int n=s.size();
for(int i=0;i<n;i++){
if(s[i]==' ')
count++;
}
// 扩充字符串s的大小,也就是每个空格替换成"%20"之后的大小
s.resize(s.size()+count*2);
int sNewSize=s.size();
// 从后先前将空格替换为"%20"
for(int i=sNewSize-1,j=n-1;j<i;i--,j--){
if(s[j]!=' ')
s[i]=s[j];
else{
s[i]='0';
s[i-1]='2';
s[i-2]='%';
i-=2;
}
}
return s;
}
};
扩展:字符串和数组有什么差别
字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,接下来我来说一说C/C++中的字符串。
在C语言中,把一个字符串存入一个数组时,也把结束符 '\0’存入数组,并以此作为该字符串是否结束的标志。
char a[5] = "asd";
for (int i = 0; a[i] != '\0'; i++) {
}
在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用’\0’来判断是否结束。
string a = "asd";
for (int i = 0; i < a.size(); i++) {
}
那么vector< char > 和 string 又有什么区别呢?
其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。
所以想处理字符串,我们还是会定义一个string类型。
例题四:翻转字符串里的单词
给定一个字符串,逐个翻转字符串中的每个单词。
示例 1:
输入: “the sky is blue”
输出: “blue is sky the”
示例 2:
输入: " hello world! "
输出: “world! hello”
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
示例 3:
输入: “a good example”
输出: “example good a”
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
链接:https://leetcode-cn.com/problems/reverse-words-in-a-string/
思路:
「这道题目可以说是综合考察了字符串的多种操作。」
一些同学会使用split库函数,分隔单词,然后定义一个新的string字符串,最后再把单词倒序相加,那么这道题题目就是一道水题了,失去了它的意义。
所以这里我还是提高一下本题的难度:「不要使用辅助空间,空间复杂度要求为O(1)。」
不能使用辅助空间之后,那么只能在原字符串上下功夫了。
想一下,我们将整个字符串都反转过来,那么单词的顺序指定是倒序了,只不过单词本身也倒叙了,那么再把单词反转一下,单词就正过来了。
所以解题思路如下:
- 移除多余空格
- 将整个字符串反转
- 将每个单词反转
思路很明确了,我们说一说代码的实现细节,就拿移除多余空格来说,一些同学会上来写如下代码:
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());
}
}
逻辑很简单,从前向后遍历,遇到空格了就erase。
如果不仔细琢磨一下erase的时间复杂度,还以为以上的代码是O(n)的时间复杂度呢。
想一下真正的时间复杂度是多少,一个erase本来就是O(n)的操作,erase实现原理题目:数组:就移除个元素很难么?,最优的算法来移除元素也要O(n)。
erase操作上面还套了一个for循环,那么以上代码移除冗余空格的代码时间复杂度为O(n^2)。
那么使用双指针法来去移除空格,最后resize(重新设置)一下字符串的大小,就可以做到O(n)的时间复杂度。
如果对这个操作比较生疏了,可以再看一下这篇文章:数组:就移除个元素很难么?是如何移除元素的。
那么使用双指针来移除冗余空格代码如下:fastIndex走的快,slowIndex走的慢,最后slowIndex就标记着移除多余空格后新字符串的长度。
void removeExtraSpaces(string& s) {
int slowIndex = 0, fastIndex = 0; // 定义快指针,慢指针
// 去掉字符串前面的空格
while (s.size() > 0 && fastIndex < s.size() && s[fastIndex] == ' ') {
fastIndex++;
}
for (; fastIndex < s.size(); fastIndex++) {
// 去掉字符串中间部分的冗余空格
if (fastIndex - 1 > 0
&& s[fastIndex - 1] == s[fastIndex]
&& s[fastIndex] == ' ') {
continue;
} else {
s[slowIndex++] = s[fastIndex];
}
}
if (slowIndex - 1 > 0 && s[slowIndex - 1] == ' ') { // 去掉字符串末尾的空格
s.resize(slowIndex - 1);
} else {
s.resize(slowIndex); // 重新设置字符串大小
}
}
代码:
class Solution {
public:
// 反转字符串s中左闭又闭的区间[start, end]
//注意s前面有引用,主程序中的s才会有变化
void reverse(string& s,int start,int end){
for(int i=start,j=end;i<j;i++,j--){
swap(s[i],s[j]);
}
}
// 移除冗余空格:使用双指针(快慢指针法)O(n)的算法
//如果使用其他方法的时间复杂度会增多
void removeExtraSpaces(string& s){
int slowIndex=0,fastIndex=0;// 定义快指针,慢指针
//去掉字符串前面的空格
while(s.size()>0&&fastIndex<s.size()&&s[fastIndex]==' '){
fastIndex++;
}
// 去掉字符串中间部分的冗余空格
for(;fastIndex<s.size();fastIndex++){
if(fastIndex-1>0&&s[fastIndex-1]==s[fastIndex]&&
s[fastIndex]==' '){
continue;
}else{
s[slowIndex++]=s[fastIndex];
}
}
//因为上面的无法去掉这种形式“abc ”
// 去掉字符串末尾的空格,但是如果有的话,也只有一个
if(slowIndex-1>0&&s[slowIndex-1]==' '){
s.resize(slowIndex-1);
}else{
s.resize(slowIndex);// 重新设置字符串大小
}
}
string reverseWords(string s) {
removeExtraSpaces(s);// 去掉冗余空格
reverse(s,0,s.size()-1);// 将字符串全部反转
//注意区间是左闭右闭
// cout<<s<<endl;
int start=0;// 反转的单词在字符串里起始位置
int end=0;// 反转的单词在字符串里终止位置
bool entry = false; // 标记枚举字符串的过程中是否已经进入了单词区间
// 开始反转单词
for(int i=0;i<s.size();i++){
if((!entry)||(s[i]!=' '&&s[i-1]==' ')){
start=i;// 确定单词起始位置
entry=true;// 确定单词起始位置
}
//单词后面有空格的情况,空格就是分词符
if((entry)&&(s[i]==' '&&s[i-1]!=' ')){
end=i-1; // 确定单词终止位置
entry=false;// 结束单词区间
reverse(s,start,end);
}
// 最后一个结尾单词之后没有空格的情况
if(entry&&(i==(s.size()-1))&&s[i]!=' '){
end=i;// 确定单词终止位置
entry=false; // 结束单词区间
reverse(s,start,end);
}
}
return s;
}
};
例题五:局部反转+整体反转
题目:剑指Offer58-II.左旋转字符串
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
示例 1:
输入: s = “abcdefg”, k = 2
输出: “cdefgab”
示例 2:
输入: s = “lrloseumgh”, k = 6
输出: “umghlrlose”
限制:
1 <= k < s.length <= 10000
链接: https://leetcode-cn.com/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/
思路
使用整体反转+局部反转就可以实现,反转单词顺序的目的。
这道题目也非常类似,依然可以通过局部反转+整体反转 达到左旋转的目的。
具体步骤为:
- 反转区间为前n的子串
- 反转区间为n到末尾的子串
- 反转整个字符串
「最后就可以得到左旋n的目的,而不用定义新的字符串,完全在本串上操作。」
例如 :示例1中 输入:字符串abcdefg,n=2
反转区间为前n的子串 :bacdefg
反转区间为n到末尾的子串:bagfedc
反转整个字符串:cdefgab
最终得到左旋2个单元的字符串:cdefgab
代码:
class Solution {
public:
//翻转的是一个左闭右闭的区间[start,end]
void reverse(string &s,int start,int end){
for(int i=start,j=end;i<=j;i++,j--){
swap(s[i],s[j]);
}
}
string reverseLeftWords(string s, int n) {
reverse(s,0,n-1);
reverse(s,n,s.size()-1);
reverse(s,0,s.size()-1);
return s;
}
};
或者
class Solution {
public:
string reverseLeftWords(string s, int n) {
reverse(s.begin(), s.begin() + n);
reverse(s.begin() + n, s.end());
reverse(s.begin(), s.end());
return s;
}
};
总结
此时我们已经反转好多次字符串了,来一起回顾一下吧。
在这篇文章字符串:这道题目,使用库函数一行代码搞定,第一次讲到反转一个字符串应该怎么做,使用了双指针法。
然后发现字符串:简单的反转还不够!,这里开始给反转加上了一些条件,当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。
后来在字符串:花式反转还不够!中,要对一句话里的单词顺序进行反转,发现先整体反转再局部反转 是一个很妙的思路。
最后再讲到本地,本题则是先局部反转再 整体反转,与字符串:花式反转还不够!类似,但是也是一种新的思路。
好了,反转字符串一共就介绍到这里,相信大家此时对反转字符串的常见操作已经很了解了。
二、KMP算法
解决问题:字符串匹配的问题
比如:
文本串:aabaabaaf
模式串:aabaaf
问在文本串中是否出现了模式串
暴力的时间复杂度O(m*n)
kmp算法要使用前缀表
aabaaf的
前缀:(不包括尾字符)
a
aa
aab
aaba
aabaa
后缀:(不包括首字母)
f
af
aaf
baaf
abaaf
前缀表:最长相等的前缀和后缀
或者是 最长公共前后缀
比如:
模式串aabaaf的前缀表:
010120
a 0
aa 1
aab 0
aaba 1
aabaa 2
aabaaf 0
数组next :有冲突的话要跳转到哪里?
求前缀表的具体代码实现:
a a b a a f
0 1 0 1 2 0 next数组
-1 0 1 0 1 2
-1 0 -1 0 1 -1
步骤:
1、初始化
2、前后缀不相同
3、前后缀相同
4、next数组
i 指向后缀末尾
j 指向前缀末尾
文字版的帮助理解:
https://www.zhihu.com/question/21923021
https://mp.weixin.qq.com/s?__biz=MzUxNjY5NTYxNA==&mid=2247484428&idx=1&sn=c0e5573f5fe3b438dbe75f93f3f164fa&scene=21#wechat_redirect
int KMP(char *t,char *p){
int i=0,j=0;
while(i<strlen(t)&&j<strlen(p)){
if(j==-1||t[i]==p[j]){
i++;
j++;}
else
j=next[j];
}
if(j==strlen(p))
return i-j;
else
return -1;
}
void getNext(char *p,int *next){
next[0]=-1;
int i=0,j=-1;
while(i<strlen(p)){
if(j==-1||p[i]==p[j]){
++i;
++j;
next[i]=j;
}
else
j=next[j];
}
}
例题一:
题目:28. 实现 strStr()
实现 strStr() 函数。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1:
输入: haystack = “hello”, needle = “ll”
输出: 2
示例 2:
输入: haystack = “aaaaa”, needle = “bba”
输出: -1
说明:
当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。
对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。
链接:https://leetcode-cn.com/problems/implement-strstr/
思路链接:
https://mp.weixin.qq.com/s?__biz=MzUxNjY5NTYxNA==&mid=2247484438&idx=1&sn=52cd12bec41d3b150d9e2651e1df0418&scene=21#wechat_redirect
代码:
class Solution {
public:
void getNext(int *next,const string& s){
int j=-1;
next[0]=j;
// 注意i从1开始
for(int i=1;i<s.size();i++){
//注意这里是while循环
while(j>=0&&s[i]!=s[j+1]){// 前后缀不相同了
j=next[j];// 向前回溯
}
// 找到相同的前后缀
if(s[i]==s[j+1]){
j++;
}
next[i]=j;// 将j(前缀的长度)赋给next[i]
}
}
int strStr(string haystack, string needle) {
if(needle.size()==0){
return 0;
}
int next[needle.size()];
getNext(next,needle);
int j=-1;// 因为next数组里记录的起始位置为-1
// 注意i就从0开始
for(int i=0;i<haystack.size();i++){
// 不匹配
while(j>=0&&haystack[i]!=needle[j+1]){
j=next[j];// j 寻找之前匹配的位置
}
// 匹配,j和i同时向后移动
if(haystack[i]==needle[j+1]){
j++;
}
// 文本串s里出现了模式串t
if(j==(needle.size()-1)){
return (i-needle.size()+1);
}
}
return -1;
}
};
例题二:
题目459.重复的子字符串
给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
示例 1:
输入: “abab”
输出: True
解释: 可由子字符串 “ab” 重复两次构成。
示例 2:
输入: “aba”
输出: False
示例 3:
输入: “abcabcabcabc”
输出: True
解释: 可由子字符串 “abc” 重复四次构成。(或者子字符串 “abcabc” 重复两次构成。)
链接:https://leetcode-cn.com/problems/repeated-substring-pattern/
思路:
最长相等前后缀的长度为:next[len - 1] + 1。
数组长度为:len。
如果len % (len - (next[len - 1] + 1)) == 0 ,则说明 (数组长度-最长相等前后缀的长度) 正好可以被 数组的长度整除,说明有该字符串有重复的子字符串。
「强烈建议大家把next数组打印出来,看看next数组里的规律,有助于理解KMP算法」
此时next[len - 1] = 7,next[len - 1] + 1 = 8,8就是此时 字符串asdfasdfasdf的最长相同前后缀的长度。
(len - (next[len - 1] + 1)) 也就是:12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 4正好可以被 12(字符串的长度) 整除,所以说明有重复的子字符串(asdf)。
代码如下:
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循环
while(j>=0&&s[i]!=s[j+1]){
j=next[j];
}
if(s[i]==s[j+1]){
j++;
}
next[i]=j;
}
}
bool repeatedSubstringPattern(string s) {
if(s.size()==0){
return false;
}
int next[s.size()];
getNext(next,s);
int len=s.size();
if(next[len-1]!=-1&&len%(len-(next[len-1]+1))==0){
return true;
}
return false;
}
};
注意:
-
erase就是O(n)的操作
-
KMP的主要思想是
「当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。」 -
前缀表有什么作用呢?
「前缀表是用来回溯的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。」 -
什么是前缀表:
「下表i之前(包括i)的字符串中,有多大长度的相同前缀后缀。」