LeetCode344. 反转字符串
题目链接:344. 反转字符串
1、思路
原始思路
C++ 中提供了反转相关的库函数 reverse()
,可以考虑直接使用相关的库函数。
思路提升
如果直接使用库函数的话,就不会清楚反转字符串的实现原理;而且这道题目也没有出现的意义。
在反转链表中,使用了双指针的方法。那么反转字符串依然是使用双指针的方法,只不过对于字符串的反转,其实要比链表简单一些。因为字符串也是一种数组,所以元素在内存中是连续分布,这就决定了反转链表和反转字符串方式上还是有所差异的。
对于字符串,我们定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。
2、代码实现
class Solution {
public:
void reverseString(vector<char>& s) {
int left = 0;
int right = s.size() - 1;
while (left < right) {
// 交换两数之数值交换法
// char temp = s[left];
// s[left] = s[right];
// s[right] = temp;
// 交换两数之位运算法
// s[left] ^= s[right];
// s[right] ^= s[left];
// s[left] ^= s[right];
// 交换两数的库函数
swap(s[left], s[right]);
left++;
right--;
}
}
};
3、复杂度分析
时间复杂度
时间复杂度: O(N)
两个指针一个从头往后,一个从后往前,直到他们相遇,总共会把长度为N的字符数组全部遍历一遍,因此时间复杂度为O(N);
空间复杂度
空间复杂度:O(1)
原地修改的字符数组,没有占用和创建其他空间,只有两个常数级的指针。
4、思考与总结
如果在现场面试中,我们什么时候使用库函数,什么时候不要用库函数呢?
- 如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。 毕竟面试官一定不是考察你对库函数的熟悉程度。
- 如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。
LeetCode541. 反转字符串II
题目链接:541. 反转字符串II
1、思路
原始思路
乍一看像是反转字符串,但是实际上题目中加上了一些特殊的规则,这道题实际上也不涉及到算法层面的知识,就是模拟实现规定的规则就可以了。
循环一遍数组,为了处理逻辑,每隔 2k 个字符的前 k 的字符,写了一堆逻辑代码或者再搞一个计数器,来统计 2k,再统计前 k 个字符。
思路提升
其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。
所以当需要固定规律一段一段去处理字符串的时候,要想想在 for 循环的表达式上做做文章。
2、代码实现
class Solution {
public:
// 反转区间为左闭右闭区间
void reverse(string& s, int start, int end) {
while (start < end) {
swap(s[start++], s[end--]);
}
}
string reverseStr(string s, int k) {
for (int i = 0; i < s.size(); i += (2 * k)) {
if (i + k <= s.size()) {
// 1. 每隔 2k 个字符的前 k 个字符进行反转
reverse(s, i, i + k - 1);
} else {
// 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符
reverse(s, i, s.size() - 1);
}
}
return s;
}
};
3、复杂度分析
时间复杂度
时间复杂度:O(N)
整个字符串长度为N,大约只有N/2的段需要处理,所以时间复杂度为O( N);
空间复杂度
空间复杂度:O(1)
原地修改的字符串,没有占用和创建其他空间。
4、思考与总结
当遇到一些边界条件不能确定的时候,直接举个栗子会让思路清晰起来。
剑指Offer 05. 替换空格
1、思路
原始思路
看到这道题第一反应是,将数组转换成链表,链表中可以自由插入和删除元素,插入元素后,再将链表转为数组。但是这种方法会使用额外的辅助空间。
思路提升
首先扩充数组到每个空格替换成"%20"之后的大小。然后从后向前替换空格,也就是双指针法。
2、代码实现
class Solution {
public:
string replaceSpace(string s) {
int count = 0;
for (char ch : s) {
if (ch == ' ') count++;
}
int old_index = s.size() - 1;
s.resize(s.size() + count * 2); // 扩充
int new_index = s.size() - 1;
for (; old_index >= 0; old_index--, new_index--) {
if (s[old_index] == ' ') {
s[new_index] = '0';
s[new_index - 1] = '2';
s[new_index - 2] = '%';
new_index -= 2;
} else {
s[new_index] = s[old_index];
}
}
return s;
}
};
3、复杂度分析
时间复杂度
时间复杂度:O(n)
- 首先统计字符串中空字符的数量,就有O(N)的复杂度
- 其次加长数组之后,仍然是O(N)级别的长度,然后指针是会遍历一遍的,所以复杂度也为O(N)
- 最后把数组转换为字符串,也是O(N)的复杂度,总体来说仍然是O(N)的时间复杂度。
空间复杂度
空间复杂度:O(1)
原地修改的字符串,没有占用和创建其他空间,只是创建几个常数级的变量。
4、思考与总结
什么要从后向前填充,从前向后填充不行么?
从前向后填充就是 O(n^2) 的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。 这么做有两个好处:
- 不用申请新数组。
- 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。
所以对于线性数据结构,填充或者删除,后序处理会高效的多。可以借这道题好好体会
5、C++语法学习
- 字符串和数组有什么差别? 字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,C/C++ 中的字符串如下所示:
-
在 C 语言中,把一个字符串存入一个数组时,也把结束符 ‘\0’ 存入数组,并以此作为该字符串是否结束的标志。
-
在 C++ 中,提供一个 string 类,string 类会提供 size 接口,可以用来判断 string 类字符串是否结束,就不用 ‘\0’ 来判断是否结束。
-
那么 vector< char > 和 string 又有什么区别呢?
其实在基本操作上没有区别,但是 string 提供更多的字符串处理的相关接口,例如 string 重载了+,而 vector 却没有,所以想处理字符串,定义一个 string 类型更加方便。
-
- string 类的使用
【注意】string 中resize()
的时间复杂度为 O(1)
151.翻转字符串里的单词
题目链接:151.翻转字符串里的单词 - 力扣
1、思路
原始思路
看到这道题,简单的思路是使用 split 库函数分隔单词,然后定义一个新的 string 字符串,最后再把单词倒序相加。但是这种方法会使用额外的辅助空间。
思路提升
我们将整个字符串都反转过来,那么单词的顺序指定是倒序了,只不过单词本身也倒序了,那么再把单词反转一下,单词不就正过来了。所以解题思路如下:
- 移除多余空格
- 将整个字符串反转
- 将每个单词反转
先讨论第一步,移除多余的空格
- 首先,字符串的前后和中间可能都有若干个多余的空格;
- 其次,比较棘手的是字符串中间的空格,在这里,我想到的是之前在数组里面做过的移除元素这道题,在那道题里面是移除特定的元素,在这里不是非常类似吗,只是这个元素变成了空格,因此,本题可以照搬那边的思路。
- 同样定义两个快慢指针,快指针用来遍历所有的元素,慢指针用来更新新的元素,这里要注意的是,单词之间需要保留有一个空格,这一点需要特殊处理。
第二步,将整个字符串反转
和之前做过的题目都一样,也是定义两个指针 left 和 right,一个从头,一个从尾,交换元素。
第三步,将其中的单词再反转
通过 for 循环找到一个个单词的区间,然后用第二步中定义的函数进行反转即可。
2、代码实现
class Solution {
public:
void removeExtraSpaces(string& s) {
int slow = 0;
for (int fast = 0; fast < s.size(); fast++) {
if (s[fast] != ' ') {
if (slow != 0) s[slow++] = ' ';
while (fast < s.size() && s[fast] != ' ') {
s[slow++] = s[fast++];
}
}
}
s.resize(slow);
}
void reverse(string& s, int start, int end) {
while (start < end) {
swap(s[start++], s[end--]);
}
}
string reverseWords(string s) {
removeExtraSpaces(s);
reverse(s, 0, s.size() - 1);
int start = 0;
for (int i = 0; i <= s.size(); i++) {
if (s[i] == ' ' || i == s.size()) {
reverse(s, start, i - 1);
start = i + 1;
}
}
return s;
}
};
3、复杂度分析
时间复杂度
每个子函数的复杂度:
removeExtraSpaces()
时间复杂度为 O(N):虽然有两个循环,但是实际上两个循环拼接起来才完整遍历一遍。reverse()
时间复杂度为 O(N);reverseWords()
时间复杂度为 O(N);
总体的时间复杂度是调用的这些子函数的时间复杂度的和,因此仍然是O(N)
空间复杂度
每个子函数的复杂度:
removeExtraSpaces()
空间复杂度为 O(1);reverse()
空间复杂度为 O(1);reverseWords()
空间复杂度为 O(1);
总体的空间复杂度是调用的这些子函数的时间复杂度的和,因此仍然是O(1)
4、思考与总结
这道题目基本把 刚刚做过的字符串操作 都覆盖了,不过就算知道解题思路,本题代码并不容易写,要多练一练。
剑指Offer58-II.左旋转字符串
1、思路
原始思路
又是一道反转字符串的题目,只不过是部分反转,刚看到这道题目的时候没有什么解题思路,只能直接看题解了。
思路提升
使用整体反转+局部反转 就可以实现反转单词顺序的目的。本题目的具体步骤如下:
- 反转区间为前n的子串
- 反转区间为n到末尾的子串
- 反转整个字符串
2、代码实现
class Solution {
public:
void reverse(string& s, int start, int end) {
while (start < end) {
swap(s[start++], s[end--]);
}
}
string reverseLeftWords(string s, int n) {
reverse(s, 0, s.size() - 1);
reverse(s, 0, s.size() -1 - n);
reverse(s, s.size() - n, s.size() -1);
return s;
}
};
3、复杂度分析
时间复杂度
时间复杂度:O(N)
每个元素就操作两次反转,所以说复杂度为O(N)
空间复杂度
空间复杂度:O(1)
原地修改的字符串,没有占用和创建其他空间。
4、思考与总结
- 题解中的解法如果没接触过的话,应该会想不到,但是做过之后,实际上和反转字符串里的单词是一个思想;
- 此时我们已经反转好多次字符串了,来一起回顾一下吧。
- 在LeetCode344中,第一次讲到反转一个字符串应该怎么做,使用了双指针法;
- 在541.反转字符串II中,这里开始给反转加上了一些条件,当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章;
- 后来在151.反转字符串中的单词,要对一句话里的单词顺序进行反转,发现先整体反转再局部反转是一个很妙的思路;
- 最后再讲到本题,本题则是先局部反转再 整体反转,与151类似,但是也是一种新的思路;
- 反转字符串一共就介绍到这里,相信大家此时对反转字符串的常见操作已经很了解了。