引言
字符串操作是编程中的基础技能,而反转字符串更是其中的常见操作。今天,我们将探讨一个更加复杂但实用的字符串反转问题:每隔k个字符反转k个字符。这个算法不仅在面试中常见,也在实际开发中有诸多应用。
目录
[1. 使用标准库函数简化代码](#1. 使用标准库函数简化代码)
[2. 递归实现](#2. 递归实现)
[3. 使用迭代器和分段处理](#3. 使用迭代器和分段处理)
[1. 文本加密](#1. 文本加密)
[2. 数据压缩预处理](#2. 数据压缩预处理)
[3. 游戏开发](#3. 游戏开发)
[4. 代码混淆](#4. 代码混淆)
[5. 网络协议](#5. 网络协议)
[1. 反转字符串中的单词](#1. 反转字符串中的单词)
[2. 旋转字符串](#2. 旋转字符串)
[3. Z字形变换](#3. Z字形变换)
[1. 减少swap操作次数](#1. 减少swap操作次数)
[2. 使用指针提高效率](#2. 使用指针提高效率)
[3. 批量处理优化](#3. 批量处理优化)
[1. 边界条件处理不当](#1. 边界条件处理不当)
[2. 忘记处理剩余字符不足k的情况](#2. 忘记处理剩余字符不足k的情况)
[3. 步长计算错误](#3. 步长计算错误)
[4. 反转逻辑错误](#4. 反转逻辑错误)
问题描述
给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。如果剩余字符少于 k 个,则将剩余字符全部反转。如果剩余字符大于等于 k 个但小于 2k 个,则反转前 k 个字符,其余字符保持原样。
示例 1:
text
输入:s = "abcdefg", k = 2
输出:"bacdfeg"
解释:前2k个字符(即前4个字符)是"abcd",反转前2个得到"bacd";
然后跳过2个,处理"efg",反转前2个得到"egf"(但实际上只有3个字符,
所以反转前2个得到"efg"?让我们仔细计算:
实际过程:每隔2k=4个字符,反转前k=2个字符
"abcdefg" -> 处理前4个"abcd":反转前2个得到"bacd"
处理后3个"efg":剩余字符少于k=2?不,有3个,大于等于k=2但小于2k=4,所以反转前2个得到"feg"
最终结果:"bacdfeg"
示例 2:
text
输入:s = "abcd", k = 2
输出:"bacd"
解释:整个字符串长度4=2k,反转前k=2个字符得到"bacd"
算法实现
cpp
class Solution {
public:
string reverseStr(string s, int k) {
int n = s.size();
// 每次处理2k个字符
for (int i = 0; i < n; i += 2 * k) {
// 计算反转区间的起始和结束位置
int begin = i;
int end = i + k - 1 >= n ? n - 1 : i + k - 1;
// 反转从begin到end的字符
while (begin < end) {
swap(s[begin], s[end]);
begin++;
end--;
}
}
return s;
}
};
算法详解
核心思路
-
遍历策略 :以
2k为步长遍历字符串 -
反转区间 :每次反转从当前位置开始的
k个字符 -
边界处理 :当剩余字符不足
k时,反转所有剩余字符
关键代码解析
cpp
int end = i + k - 1 >= n ? n - 1 : i + k - 1;
-
i + k - 1:计算理论上的反转结束位置 -
>= n:检查是否超出字符串长度 -
? n - 1:如果超出,则取字符串最后一个字符 -
: i + k - 1:否则,取理论上的结束位置
图解算法
text
示例:s = "abcdefg", k = 2, n = 7
第一步:i = 0
begin = 0
end = min(0+2-1, 6) = min(1, 6) = 1
反转s[0]和s[1]:"ab" -> "ba"
字符串变为:"bacdefg"
第二步:i = i + 2*k = 0 + 4 = 4
begin = 4
end = min(4+2-1, 6) = min(5, 6) = 5
反转s[4]和s[5]:"ef" -> "fe"
字符串变为:"bacdfeg"
第三步:i = 4 + 4 = 8,超过n=7,循环结束
最终结果:"bacdfeg"
时间复杂度与空间复杂度
时间复杂度:O(n)
-
每个字符最多被访问两次(一次在反转中,一次在遍历中)
-
总操作次数与字符串长度成正比
空间复杂度:O(1)
-
只使用了常数个额外变量
-
原地修改,不需要额外存储空间
算法优化与变种
1. 使用标准库函数简化代码
cpp
class Solution {
public:
string reverseStr(string s, int k) {
int n = s.size();
for (int i = 0; i < n; i += 2 * k) {
// 计算反转的结束位置
int end = min(i + k, n);
// 使用reverse函数反转子串
reverse(s.begin() + i, s.begin() + end);
}
return s;
}
};
2. 递归实现
cpp
class Solution {
public:
string reverseStr(string s, int k) {
int n = s.size();
if (n <= k) {
// 如果字符串长度小于等于k,直接反转整个字符串
reverse(s.begin(), s.end());
return s;
} else if (n <= 2 * k) {
// 如果字符串长度大于k但小于等于2k,反转前k个字符
reverse(s.begin(), s.begin() + k);
return s;
} else {
// 处理前2k个字符,然后递归处理剩余部分
// 反转前k个字符
reverse(s.begin(), s.begin() + k);
// 递归处理从位置2k开始的部分
return s.substr(0, 2 * k) + reverseStr(s.substr(2 * k), k);
}
}
};
3. 使用迭代器和分段处理
cpp
class Solution {
public:
string reverseStr(string s, int k) {
auto it = s.begin();
while (it != s.end()) {
auto segment_end = it + min(static_cast<int>(distance(it, s.end())), 2 * k);
auto reverse_end = it + min(static_cast<int>(distance(it, segment_end)), k);
// 反转当前段的前k个字符
reverse(it, reverse_end);
// 移动到下一个2k段
it = segment_end;
}
return s;
}
};
边界条件与测试用例
全面测试用例
cpp
void testReverseStr() {
Solution solution;
// 基础测试
assert(solution.reverseStr("abcdefg", 2) == "bacdfeg");
assert(solution.reverseStr("abcd", 2) == "bacd");
// 边界测试
assert(solution.reverseStr("", 2) == ""); // 空字符串
assert(solution.reverseStr("a", 1) == "a"); // 单个字符,k=1
assert(solution.reverseStr("ab", 1) == "ab"); // 两个字符,k=1
assert(solution.reverseStr("abc", 1) == "abc"); // 三个字符,k=1
// 特殊k值测试
assert(solution.reverseStr("abcdefg", 1) == "abcdefg"); // k=1,每隔2个字符反转1个,实际上没有变化
assert(solution.reverseStr("abcdefg", 3) == "cbadefg"); // k=3
assert(solution.reverseStr("abcdefgh", 3) == "cbadefhg"); // k=3,长度8
// 字符串长度小于k
assert(solution.reverseStr("abc", 5) == "cba"); // 长度3 < k=5,反转全部
// 字符串长度等于k
assert(solution.reverseStr("abcde", 5) == "edcba"); // 长度5 = k=5,反转全部
// 字符串长度在k和2k之间
assert(solution.reverseStr("abcdef", 4) == "dcbaef"); // 长度6,k=4,2k=8,反转前4个
// 长字符串测试
assert(solution.reverseStr("abcdefghijklmnopqrstuvwxyz", 5) == "edcbafghijklponmrqtsvuxwzy");
cout << "所有测试通过!" << endl;
}
边界条件分析
-
空字符串:直接返回空字符串
-
k=0:题目通常不会给出k=0,但需要处理(如果k=0,不进行任何反转)
-
k大于字符串长度:反转整个字符串
-
字符串长度正好是2k的倍数:完全按照规则处理
-
字符串长度不是2k的倍数:正确处理剩余字符
实际应用场景
1. 文本加密
-
简单的字符串混淆算法
-
在文本传输中增加一层简单加密
2. 数据压缩预处理
-
某些压缩算法在处理前会对数据进行特定的重排
-
提高压缩效率的预处理步骤
3. 游戏开发
-
文字谜题游戏中的字符串变换
-
生成特定模式的字符串
4. 代码混淆
-
对字符串常量进行简单变换
-
增加反编译的难度
5. 网络协议
-
某些协议对数据包进行特定格式的重组
-
简单的数据包混淆
扩展问题
1. 反转字符串中的单词
cpp
class Solution {
public:
string reverseWords(string s) {
int n = s.size();
int start = 0;
for (int i = 0; i <= n; i++) {
// 遇到空格或字符串结束,反转单词
if (i == n || s[i] == ' ') {
// 反转从start到i-1的字符
int begin = start, end = i - 1;
while (begin < end) {
swap(s[begin], s[end]);
begin++;
end--;
}
start = i + 1; // 更新下一个单词的起始位置
}
}
return s;
}
};
2. 旋转字符串
cpp
class Solution {
public:
string rotateString(string s, int k) {
int n = s.size();
if (n == 0) return s;
k = k % n; // 处理k大于n的情况
// 三次反转法
// 1. 反转整个字符串
reverse(s.begin(), s.end());
// 2. 反转前k个字符
reverse(s.begin(), s.begin() + k);
// 3. 反转剩余部分
reverse(s.begin() + k, s.end());
return s;
}
};
3. Z字形变换
cpp
class Solution {
public:
string convert(string s, int numRows) {
if (numRows == 1) return s;
vector<string> rows(min(numRows, int(s.size())));
int curRow = 0;
bool goingDown = false;
for (char c : s) {
rows[curRow] += c;
if (curRow == 0 || curRow == numRows - 1) {
goingDown = !goingDown;
}
curRow += goingDown ? 1 : -1;
}
string result;
for (string row : rows) {
result += row;
}
return result;
}
};
性能优化技巧
1. 减少swap操作次数
cpp
class Solution {
public:
string reverseStr(string s, int k) {
int n = s.size();
for (int i = 0; i < n; i += 2 * k) {
int left = i;
int right = min(i + k - 1, n - 1);
// 使用异或交换,减少临时变量
while (left < right) {
s[left] ^= s[right];
s[right] ^= s[left];
s[left] ^= s[right];
left++;
right--;
}
}
return s;
}
};
2. 使用指针提高效率
cpp
class Solution {
public:
string reverseStr(string s, int k) {
char* str = &s[0];
int n = s.size();
for (int i = 0; i < n; i += 2 * k) {
int left = i;
int right = min(i + k - 1, n - 1);
while (left < right) {
char temp = str[left];
str[left] = str[right];
str[right] = temp;
left++;
right--;
}
}
return s;
}
};
3. 批量处理优化
cpp
class Solution {
public:
string reverseStr(string s, int k) {
int n = s.size();
// 预先计算反转次数,避免每次循环都计算min
int segments = (n + 2 * k - 1) / (2 * k); // 向上取整
for (int seg = 0; seg < segments; seg++) {
int start = seg * 2 * k;
int end = min(start + k - 1, n - 1);
int left = start, right = end;
while (left < right) {
swap(s[left], s[right]);
left++;
right--;
}
}
return s;
}
};
常见错误与注意事项
1. 边界条件处理不当
cpp
// 错误示例:没有正确处理i+k-1可能为负数的情况
int end = i + k - 1; // 当k=0时,i+k-1可能为负数
2. 忘记处理剩余字符不足k的情况
cpp
// 错误示例:假设总是有k个字符可以反转
for (int i = 0; i < n; i += 2*k) {
int begin = i;
int end = i + k - 1; // 当i+k-1>=n时越界
// ...
}
3. 步长计算错误
cpp
// 错误示例:步长应该是2k,而不是k
for (int i = 0; i < n; i += k) { // 错误:应该是i += 2*k
// ...
}
4. 反转逻辑错误
cpp
// 错误示例:反转了错误的区间
// 应该是反转前k个,而不是后k个
int begin = i + k; // 错误:应该是int begin = i;
int end = i + 2*k - 1; // 错误
总结
反转字符串的进阶算法展示了如何将简单的字符串反转操作扩展为更复杂的模式处理。通过这个问题,我们学习了:
-
分块处理思想:将大问题分解为可处理的小块
-
边界条件处理:正确处理各种边界情况
-
循环步长技巧:使用适当的步长遍历数据
-
原地修改算法:在不使用额外空间的情况下修改数据
关键要点:
-
以
2k为步长遍历字符串 -
每次反转
min(k, 剩余字符数)个字符 -
注意处理各种边界条件
-
选择合适的算法实现方式
掌握这种字符串处理模式不仅有助于解决类似问题,还能提高对循环、边界条件和算法优化的理解。在实际开发中,这种"分块处理"的思想可以应用于许多其他场景,如数据分页、批量处理、并行计算等。
记住:优秀的算法不仅在于正确性,还在于清晰性、效率和鲁棒性。