【算法60天:Day8】344.反转字符串、541.反转字符串||、 剑指Offer 05.替换空格、 151.翻转字符串里的单词、 剑指Offer58-II.左旋转字符串
字符串
字符串和数组有什么差别
字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定。
-
在C语言中,把一个字符串存入一个数组时,也把结束符 '\0’存入数组,并以此作为该字符串是否结束的标志。
-
在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用’\0’来判断是否结束。
-
那么vector< char > 和 string 又有什么区别呢?
其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。
所以想处理字符串,我们还是会定义一个string类型。
-
-
基本操作
- 构造
#include<string> string s0 = "Initial String"; string s1; string s2(s0); string s3(s0,pos:8,n:3); string s4(s:"A ");
-
操作
- 1.访问
string str = "hello world"; for(int i =0; i<str.size(); ++i) { cout << str[i]; }
- 2.插入
string str1 = "to be question"; string str2 = "that is a "; string str3 = "or not world"; string str4; str4.insert(pos1:0,str1); str4.insert(pos1:6,str3,pos2:0,n:7); str4.insert();
- 3.移除
str4.erase(pos:19); str4.erase(pos:0,n:9);
- 4.清空
str4.clear();
-
运算
-
函数
1.长度int length = str1.size();
2.查找
find
3.子串
string str2 = str1.substr(pos:13); string str3 = str1.substr(pos:13,n:3);
不要太迷恋库函数,如果在现场面试中,我们什么时候使用库函数,什么时候不要用库函数呢?(卡哥建议)
- 如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。
- 如果库函数仅仅是解题过程的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,就可以考虑使用库函数。
平时练习也本着这样的原则练习,有助于对算法的理解。
题目
经常考的类型:
- 遍历
- 加密
- 统计
- 匹配
反转字符串(344)
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例1
输入:s = [“h”,“e”,“l”,“l”,“o”]
输出:[“o”,“l”,“l”,“e”,“h”]
示例2
输入:s = [“H”,“a”,“n”,“n”,“a”,“h”]
输出:[“h”,“a”,“n”,“n”,“a”,“H”]
链接:https://leetcode.cn/problems/reverse-string
解题思路
定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。
C++
class Solution {
public:
void reverseString(vector<char>& s) {
for (int i = 0, j = s.size() - 1; i < s.size()/2; i++, j--) {
swap(s[i],s[j]);
}
}
};
C
void reverseString(char* s,int sSize)
{
int left = 0;
int right = sSize - 1;
while(left < right)
{
char temp = s[left];
s[left++] = s[right];
s[right--] = temp;
}
}
补充
swap可以有两种实现
一种就是常见的交换数值:
int tmp = s[i];
s[i] = s[j];
s[j] = tmp;
一种就是通过位运算(这种之前没了解过,可能见过,忘记了):
s[i] ^= s[j];
s[j] ^= s[i];
s[i] ^= s[j];
反转字符串||(541)
给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。
如果剩余字符少于 k 个,则将剩余字符全部反转。
如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
示例1
输入:s = “abcdefg”, k = 2
输出:“bacdfeg”
示例2
输入:s = “abcd”, k = 2
输出:“bacd”
链接https://leetcode.cn/problems/reverse-string-ii
解题思路
这道题目其实也是模拟,实现题目中规定的反转规则就可以了。
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 );
} else {
// 3. 剩余字符少于 k 个,则将剩余字符全部反转。
reverse(s.begin() + i, s.end());
}
}
return s;
}
};
C
char * reverseStr(char * s, int k){
int len = strlen(s);
for (int i = 0; i < len; i += (2 * k)) {
//判断剩余字符是否少于 k
k = i + k > len ? len - i : k;
int left = i;
int right = i + k - 1;
while (left < right) {
char temp = s[left];
s[left++] = s[right];
s[right--] = temp;
}
}
return s;
}
翻转字符串里的单词(151)
给定一个字符串,逐个翻转字符串中的每个单词。
示例1
输入:s = “the sky is blue”
输出:“blue is sky the”
示例2
输入:s = " hello world "
输出:“world hello”
解释:反转后的字符串中不能存在前导空格和尾随空格。
示例3
输入:s = “a good example”
输出:“example good a”
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。
链接https://leetcode.cn/problems/reverse-words-in-a-string
解题思路
思路一:用split库函数,分隔单词,然后定义一个新的string字符串,最后再把单词倒序相加,那么这道题题目就是一道水题了,失去了它的意义。
所以这里我还是提高一下本题的难度:不要使用辅助空间,空间复杂度要求为O(1)。
思路二:
1.移除空格问题
数组中移除元素
for
erase
O(n2)
O(n)
空间复杂度O1
2.整个字符串全反转
3.再针对每个单词再做一次反转
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;
}
};
左旋转字符串(剑指Offer58-||)
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
示例1
输入: s = “abcdefg”, k = 2
输出: “cdefgab”
示例2
输入: s = “lrloseumgh”, k = 6
输出: “umghlrlose”
链接https://leetcode.cn/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof
解题思路
思路一:使用substr,来做这道题。 其实使用substr 和 反转 时间复杂度是一样的 ,都是O(n),但是使用substr申请了额外空间,所以空间复杂度是O(n),而反转方法的空间复杂度是O(1)。
如果想让这套题目有意义,就不要申请额外空间。
思路二:
1.反转区间为前n的子串
2.反转区间为n到末尾的子串
3.反转整个字符串
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;
}
};
替换空格(剑指Offer05)
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
示例1
输入:s = “We are happy.”
输出:“We%20are%20happy.”
链接https://leetcode.cn/problems/ti-huan-kong-ge-lcof/
解题思路
首先扩充数组到每个空格替换成"%20"之后的大小。
然后从后向前替换空格,也就是双指针法,过程如下:
i指向新长度的末尾,j指向旧长度的末尾。
注1
很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
这么做有两个好处:
1.不用申请新数组。
2.从后向前填充元素,避免了从前先后填充元素要来的 每次添加元素都要将添加元素之后的所有元素向后移动。
时间复杂度,空间复杂度均超过100%的用户。
class Solution {
public:
string replaceSpace(string s) {
int count = 0; // 统计空格的个数
int sOldSize = s.size();
for (int i = 0; i < s.size(); i++) {
if (s[i] == ' ') {
count++;
}
}
// 扩充字符串s的大小,也就是每个空格替换成"%20"之后的大小
s.resize(s.size() + count * 2);
int sNewSize = s.size();
// 从后先前将空格替换为"%20"
for (int i = sNewSize - 1, j = sOldSize - 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;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
char* replaceSpace(char* s){
//统计空格数量
int count = 0;
int len = strlen(s);
for (int i = 0; i < len; i++) {
if (s[i] == ' ') {
count++;
}
}
//为新数组分配空间
int newLen = len + count * 2;
char* result = malloc(sizeof(char) * newLen + 1);
//填充新数组并替换空格
for (int i = len - 1, j = newLen - 1; i >= 0; i--, j--) {
if (s[i] != ' ') {
result[j] = s[i];
} else {
result[j--] = '0';
result[j--] = '2';
result[j] = '%';
}
}
result[newLen] = '\0';
return result;
}
总结
1.双指针
此时算上本题,已经做了七道双指针相关的题目了,注意总结复盘。
移除元素(27)
三数之和(15)
四数之和(18)
翻转链表(206)
环形链表II(142)
反转字符串(344)
2.反转字符串
在反转字符串(344),第一次遇到反转一个字符串应该怎么做,使用了双指针法。
然后发现反转字符串II(541),这里开始给反转加上了一些条件,当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。
后来在**翻转字符串里的单词(151)**中,要对一句话里的单词顺序进行反转,发现先整体反转再局部反转 是一个很妙的思路。
最后再到左旋转字符串(剑指Offer58-II),则是先局部反转再整体反转,与**翻转字符串里的单词(151)**类似,但是也是一种新的思路。
1.双指针
此时算上本题,已经做了七道双指针相关的题目了,注意总结复盘。
移除元素(27)
三数之和(15)
四数之和(18)
翻转链表(206)
环形链表II(142)
反转字符串(344)
2.反转字符串
在反转字符串(344),第一次遇到反转一个字符串应该怎么做,使用了双指针法。
然后发现反转字符串II(541),这里开始给反转加上了一些条件,当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。
后来在**翻转字符串里的单词(151)**中,要对一句话里的单词顺序进行反转,发现先整体反转再局部反转 是一个很妙的思路。
最后再到左旋转字符串(剑指Offer58-II),则是先局部反转再整体反转,与**翻转字符串里的单词(151)**类似,但是也是一种新的思路。