1. 算法刷题实战:字符串操作三连击
今天继续我的力扣刷题之旅,这次集中攻克了三道字符串相关的题目:344反转字符串、541反转字符串II和54替换数字。这三道题看似基础,但涵盖了字符串操作中最常见的几种场景,特别适合用来巩固双指针技巧和边界条件处理能力。
作为程序员,字符串处理是日常开发中最频繁遇到的任务之一。从简单的数据清洗到复杂的文本解析,都离不开扎实的字符串操作基本功。这三道题目虽然来自力扣不同难度层级,但把它们放在一起练习,能帮助我们建立处理字符串问题的系统性思维。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 344 反转字符串:双指针经典应用
2.1 题目解析与思路
反转字符串可能是所有编程初学者接触的第一个算法问题,但它的重要性不容小觑。题目要求我们原地修改输入的字符数组,将其内容反转。注意是字符数组(vector
最直观的解法就是使用双指针技巧:
- 左指针从数组头部开始
- 右指针从数组尾部开始
- 两两交换指针所指元素
- 左指针右移,右指针左移
- 直到两指针相遇或交叉
这个算法的关键在于:
- 遍历只需要进行到数组长度的一半即可(size/2)
- 交换操作需要借助临时变量
- 原地修改意味着空间复杂度为O(1)
2.2 代码实现与优化
cpp复制class Solution {
public:
void reverseString(vector<char>& s) {
char tmp;
for(int i = 0; i < s.size()/2; i++) {
tmp = s[i];
s[i] = s[s.size()-1-i];
s[s.size()-1-i] = tmp;
}
}
};
这里有几个值得注意的实现细节:
- 循环终止条件是i < s.size()/2,而不是i <= s.size()/2,这样可以正确处理奇偶长度的字符串
- 使用size()-1-i来计算对称位置,避免定义额外的右指针变量
- 临时变量tmp的作用域限定在循环内部,减少内存占用
提示:在C++中,vector的size()方法返回的是size_t类型(无符号整数),当vector为空时,size()-1会产生一个很大的正数而非-1。虽然本题不需要特别处理,但在其他场景下需要注意这个边界情况。
2.3 复杂度分析与变种
时间复杂度:O(n),每个元素被访问一次
空间复杂度:O(1),只使用了固定数量的额外空间
这个基础算法可以衍生出许多变种问题,比如:
- 反转字符串中的元音字母
- 反转字符串中的单词顺序
- 旋转字符串(部分反转)
理解这个基础版本后,这些变种问题都能迎刃而解。
3. 541 反转字符串II:分段处理技巧
3.1 题目特殊要求解析
这道题是反转字符串的变种,要求每计数至2k个字符,就反转这2k字符中的前k个字符。如果剩余字符少于k个,则将剩余字符全部反转;如果剩余字符小于2k但大于或等于k个,则反转前k个字符。
这种分段处理的需求在实际开发中很常见,比如:
- 批量处理数据时分块操作
- 分页显示时的数据切割
- 网络传输时的数据分包
3.2 算法设计与实现
关键思路是:
- 以2k为步长遍历字符串
- 对每个子区间[i, i+2k),反转前k个字符
- 处理剩余字符的特殊情况
cpp复制class Solution {
public:
string reverseStr(string s, int k) {
for (int i = 0; i < s.size(); i += 2 * k) {
if (i + k <= s.size()) {
reverse(s.begin() + i, s.begin() + i + k);
} else {
reverse(s.begin() + i, s.end());
}
}
return s;
}
};
实现要点:
- 使用标准库的reverse函数简化代码
- 判断条件i + k <= s.size()来确定是否有完整的k个字符需要反转
- 步进量为2k(i += 2 * k)
3.3 边界条件处理
这类分段处理问题最容易出错的就是边界条件。需要特别注意:
- 字符串长度不是k的整数倍时
- k=0或k=1的特殊情况(虽然题目通常保证k>0)
- 空字符串的情况
在实际面试中,即使题目保证输入有效,主动讨论这些边界条件也能展示你的严谨性。
4. 54 替换数字:字符串扩容与双指针
4.1 问题特殊性分析
这道题来自卡码网的模拟笔试,要求将字符串中的每个数字字符替换为"number"。特殊之处在于替换后的字符串会变长,这带来了两个挑战:
- 原字符串空间不足,需要扩容
- 原地修改时需要移动字符
这与常规的替换操作不同,常规替换通常是等长替换(如空格替换为%20,长度不变),而这种增长型替换需要考虑内存重新分配和元素移动。
4.2 两种解法对比
解法一:新建字符串拼接
cpp复制#include <iostream>
#include <string>
using namespace std;
int main() {
char s;
string result;
while(cin >> s) {
if(s >= '0' && s <= '9') {
result += "number";
} else {
result += s;
}
}
cout << result << endl;
}
这种方法简单直观,但空间复杂度为O(n),不是最优解。适用于:
- 对空间要求不严格的场景
- 字符串长度不大的情况
- 需要快速实现的场合
解法二:双指针原地修改
更高效的解法是先统计数字个数,扩展字符串大小,然后从后向前处理:
- 统计数字字符个数count
- 调整字符串大小:s.resize(s.size() + count*5)
- 使用双指针:
- 左指针i指向原字符串末尾
- 右指针j指向新字符串末尾
- 从后向前遍历:
- 遇到数字:逆序插入"number",j移动6位
- 遇到非数字:直接复制,j移动1位
- i每次移动1位
这种方法的优势:
- 空间复杂度O(1)(原地修改)
- 只需要一次遍历统计和一次遍历处理
- 避免了频繁的内存分配
4.3 双指针实现细节
cpp复制// 伪代码示意
int count = 统计数字个数;
s.resize(s.size() + count * 5);
int i = 原末尾, j = 新末尾;
while (i >= 0) {
if (s[i]是数字) {
// 反向插入"number"
s[j--] = 'r';
s[j--] = 'e';
s[j--] = 'b';
s[j--] = 'm';
s[j--] = 'u';
s[j--] = 'n';
} else {
s[j--] = s[i];
}
i--;
}
实际实现时需要注意:
- 字符串扩容的正确计算
- 指针移动的步长控制
- 反向插入时的字符顺序
- 终止条件的判断
5. 刷题经验与技巧分享
5.1 字符串问题通用解法
通过这三道题目,我总结了处理字符串问题的几个通用技巧:
-
双指针法:适用于需要原地修改、反转或特定条件交换的场景
- 快慢指针:处理删除、去重等问题
- 左右指针:处理反转、对称等问题
-
边界条件检查:
- 空字符串
- 奇数/偶数长度
- 特殊字符处理
- 缓冲区溢出风险
-
复杂度优化:
- 尽量减少不必要的字符串拷贝
- 合理使用标准库函数(如reverse)
- 考虑从后向前处理的可行性
5.2 调试技巧
在解决字符串问题时,我常用的调试方法:
- 可视化追踪:在纸上画出字符串和指针位置的变化
- 极端用例测试:
- 空字符串
- 全数字/全字母字符串
- 超长字符串
- 逐步打印:在关键步骤打印字符串和指针状态
5.3 常见错误警示
根据我的踩坑经验,字符串问题最容易犯的错误:
- 索引越界:特别是在处理边界时忘记检查size()
- 循环条件错误:如该用<时用了<=
- 字符与数字混淆:如直接比较'9' > 'A'(实际是ASCII比较)
- 内存分配不足:如替换时没有预留足够空间
6. 扩展思考与实际应用
这三道题目虽然简单,但它们的变种在实际开发中非常常见:
- 数据清洗:如处理用户输入时替换特定字符
- 文本格式化:如按特定规则重新排列文本
- 协议处理:如网络数据包的分段和重组
- 编码转换:如处理不同编码的字符串转换
我在实际项目中就遇到过类似541题的需求:需要每隔一定长度对日志信息进行格式化处理,以便于阅读和分析。理解这些基础算法后,解决这类实际问题就得心应手了。
