文章目录
- 344.反转字符串
- 思路
- 541.反转字符串 II
- 思路
- 卡码网:54.替换数字
- 思路
- 复习:字符串 vs 数组
- 总结
今天是字符串专题的第一天,主要是一些基础的题目
344.反转字符串
建议: 本题是字符串基础题目,就是考察 reverse 函数的实现,同时也明确一下 平时刷题什么时候用 库函数,什么时候 不用库函数
题目链接:344. 反转字符串 - 力扣(LeetCode)
本题要编写一个函数,将输入的字符串反转过来。这道题考察的是reverse函数的实现原理,就不要直接使用reverse函数解题了。如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数;如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数
思路
这道题的思想与 206.反转链表 相同,都是使用双指针法字符串也是一种数组,所以元素再内存中是连续存储的,因此 反转字符串 比 反转链表要容易
整体思路:
定义left、right指针(就是索引下标)
- 初始:left指向字符串的第一个元素,right指向字符串的最后一个元素
- 反转:交换
s[left]
与s[right]
,然后收缩这两个指针:left++
、right--
- 循环条件:当
right > left
时,就继续执行while循环,交换s[left]
与s[right]
反转过程 如图:
可以使用库函数swap执行交换操作,swap函数可以有两种实现
-
一种是常见的交换数值:
char tmp = s[left]; s[left] = s[right]; s[right] = tmp;
-
另一种是通过位运算,CSAPP第二章的位运算部分讲过这个:
s[left] ^= s[right]; s[right] ^= s[left]; s[left] ^= s[right]
代码实现
class Solution {
public:
void reverseString(vector<char>& s) {
int left = 0, right = s.size()-1;
while(right > left)
{
swap(s[left], s[right]);
right--;
left++;
}
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
541.反转字符串 II
建议:本题又进阶了,自己先去独立做一做,然后在看题解,对代码技巧会有很深的体会。
题目链接:541. 反转字符串 II - 力扣(LeetCode)
思路
这道题就是设置了一点障碍,思想与前一道题是相同的,只是反转并不是一次完成的,每2k个字符才能反转一次。一种思路是将字符串“分割”:遍历字符串,设置count计数,每计数2k个字符,就进入反转程序,对这2k个字符的区间进行反转。另一种思路是在遍历的同时直接进行反转,对于一些特殊的情况我们单独判断。第二种思路更自然,我们选择第二种思路解题
整体思路:
如果使用 344.反转字符串 中的双指针法反转字符串,我们使用current表示当前遍历到的位置:
初始:current指向字符串的第一个元素
反转:每次while循环开始,left赋为current,是反转区间的第一个元素。关键是确定right,剩余字符串的长度length 与 k 的大小比较决定了right的位置
- 若length < k,题目要求将剩余字符全部反转。此时right = s.size()-1,指向这个字符串的末尾
- 若k <= length < 2k,题目要求反转前 k 个字符,其余字符保持原样,那么right = current + k -1,指向反转区间的第k个字符
- 若k >= 2k,题目要求反转这 2k 个字符中的前 k 个字符,则right = current + k - 1,指向反转区间的第k个字符
当完成一个区间的反转后,current向后移动2k个元素,即current += 2*k。如果 leng < k 或 k <= length < 2k,则移动2k个元素后current > s.size(),因此不会进入下一次循环。这保证了k <= length < 2k时,只反转前k个字符,而其他字符保持原样
代码实现
class Solution {
public:
string reverseStr(string s, int k) {
// 使用current指针指向当前遍历到的位置
int current = 0;
int left, right;
while(current < s.size())
{
left = current;
int length = s.size() - current;
if(length < k)
{
right = s.size()-1;
}else // 剩下的两种情况都是反转前k个字符
{
right = current + k - 1;
}
while(right > left)
{
swap(s[left], s[right]);
right--;
left++;
}
current += 2*k; // 如果剩余字符小于k个 或 小于 2k 但大于或等于 k 个,则下次循环直接退出了
}
return s;
}
};
本题的关键在于对反转区间的划分,而不在于如何反转,因此可以直接使用reverse函数
代码实现
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;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
卡码网:54.替换数字
建议:对于线性数据结构,填充或者删除,后序处理会高效的多。好好体会一下
这道题是Carl刷题网站上的,题目链接:54. 替换数字(第八期模拟笔试) (kamacoder.com)
这个题目需要自己补全所有代码,包括导入库,实现main函数
思路
本题使用双指针法。我们首先遍历一遍字符串,统计所有出现过的数字字符数量,然后对这个字符串做扩展,预留需要填充的大小,如图所示:
然后我们利用双指针法在这个字符串上进行数字的替换,我们需要明确这两个指针的含义:
-
第一个指针指向旧字符串的末尾(未扩展前的字符串),用来判断旧字符串中的字符是否为数字
-
第二个指针指向新字符串的末尾,根据第一个指针指向的字符,填充相应的元素:
- 如果第一个指针指向的是字母字符,则第二个指针填充这个字母字符,这两个指针同时向前移动
- 如果第一个指针指向的是数字字符,则第二个指针填充“number”,然后两个指针同时向前移动
其实整个过程可以看成是一个映射:
- 第一个指针指向字母字符 -> 第二个指针填充字母字符
- 第一个指针指向数字字符 -> 第二个指针填充"number"
思路如图:
你会发现我们是从后向前填充这个数组的,从前向后填充不行吗?
从前向后填充就是O(n2)的算法了,因为每次添加元素都要将添加元素之后的所有元素整体向后移动,注意:我们的旧字符串 在 新字符串的前面部分,因此从头填充是需要将旧字符串依次后移,否则会丢失旧字符串中的字符
其实很多数组填充类的问题,其做法都是先预先给数组扩容带填充后的大小,然后在从后向前进行操作
这么做有两个好处:
- 不用申请新数组
- 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题
代码实现
#include<iostream>
using namespace std;
int main()
{
// 由于题目可能逐个输入字符串,所以我们使用while循环读取输入的字符
string s;
while(cin >> s)
{
int sOldIndex = s.size()-1; // 第一个指针,初始指向旧字符串的末尾元素
int count = 0; // 统计数字字符的个数
for(int i=0; i<s.size(); ++i)
{
if(s[i] >= '0' && s[i] <= '9')
{
count++;
}
}
// 扩充字符串s的大小,也就是将每个数字替换为"number"之后的字符串大小
s.resize(s.size() + count*5);
int sNewIndex = s.size() - 1;
// 从后向前将数字替换为"number",字母直接映射过来
while(sOldIndex >= 0)
{
if(s[sOldIndex] >= '0' && s[sOldIndex] <= '9')
{
s[sNewIndex--] = 'r';
s[sNewIndex--] = 'e';
s[sNewIndex--] = 'b';
s[sNewIndex--] = 'm';
s[sNewIndex--] = 'u';
s[sNewIndex--] = 'n';
}else
{
s[sNewIndex--] = s[sOldIndex];
}
// sOldIndex向前移动一位
sOldIndex--;
}
cout << s << endl;
}
}
复习:字符串 vs 数组
复习一下字符串和数组有什么差别:
字符串是若干字符组成的有限序列,也可以理解是一个字符数组,但是很多语言对字符串做了特殊的规定,接下来说一说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类型
总结
今天的题目比较简单,关键是双指针法和字符串的操作,最后一道题是一道经典的数组扩充题目,需要好好体会
字符串的一些常用操作如下:
#include <iostream>
#include <string>
int main() {
// 1. 构造字符串
std::string s1; // 默认构造函数
std::string s2("Hello, World!"); // 从C风格字符串构造
std::string s3(s2); // 复制构造函数
std::string s4(s2, 7, 5); // 从 s2 的第 7 个字符开始取 5 个字符
// 2. 赋值操作
s1 = s2; // 赋值运算符
s3.assign("Hi there!"); // assign 赋值
// 3. 访问元素
char c1 = s2[1]; // 使用 operator[] 访问元素,结果为 'e'
char c2 = s2.at(1); // 使用 at 访问元素,结果为 'e'
char c3 = s2.front(); // 访问第一个字符,结果为 'H'
char c4 = s2.back(); // 访问最后一个字符,结果为 '!'
// 4. 迭代器
std::cout << "Using iterator: ";
for (auto it = s2.begin(); it != s2.end(); ++it) {
std::cout << *it; // 使用迭代器输出字符串
}
std::cout << std::endl;
std::cout << "Using reverse iterator: ";
for (auto rit = s2.rbegin(); rit != s2.rend(); ++rit) {
std::cout << *rit; // 使用反向迭代器输出字符串
}
std::cout << std::endl;
// 5. 容量相关
bool isEmpty = s2.empty(); // 检查字符串是否为空,结果为 false
std::size_t size = s2.size(); // 获取字符串长度,结果为 13
std::size_t length = s2.length(); // 获取字符串长度,结果为 13
std::size_t capacity = s2.capacity(); // 获取容量
s2.reserve(50); // 预留至少 50 个字符的存储空间
s2.resize(10); // 将字符串大小调整为 10
// 6. 修改操作
s2.clear(); // 清空字符串
s2 = "Hello, World!"; // 重新赋值
s2.push_back('!'); // 在末尾添加字符
s2.pop_back(); // 移除末尾的字符
s2.insert(5, " World"); // 在位置 5 插入字符串
s2.erase(5, 6); // 从位置 5 开始删除 6 个字符
s2.replace(7, 5, "C++"); // 从位置 7 开始替换 5 个字符为 "C++"
// 7. 查找操作
std::size_t pos = s2.find("C++"); // 查找子字符串 "C++" 的第一次出现位置
std::size_t rpos = s2.rfind("l"); // 从后向前查找字符 'l' 的位置
std::size_t fpos = s2.find_first_of("aeiou"); // 查找第一个元音字母的位置
std::size_t lpos = s2.find_last_of("aeiou"); // 查找最后一个元音字母的位置
std::size_t fnpos = s2.find_first_not_of("Hello"); // 查找第一个不在 "Hello" 中的字符位置
std::size_t lnpos = s2.find_last_not_of("Hello"); // 查找最后一个不在 "Hello" 中的字符位置
// 8. 子字符串
std::string sub = s2.substr(7, 5); // 从位置 7 开始获取长度为 5 的子字符串
// 9. 比较操作
int result = s2.compare(s3); // 比较 s2 和 s3 的内容
// 输出结果
std::cout << "s2: " << s2 << std::endl;
std::cout << "pos: " << pos << std::endl;
std::cout << "rpos: " << rpos << std::endl;
std::cout << "fpos: " << fpos << std::endl;
std::cout << "lpos: " << lpos << std::endl;
std::cout << "fnpos: " << fnpos << std::endl;
std::cout << "lnpos: " << lnpos << std::endl;
std::cout << "sub: " << sub << std::endl;
std::cout << "compare result: " << result << std::endl;
return 0;
}
感谢chatgpt😸