引言
字符串中的单词反转是一个经典问题,它考察了对字符串操作的熟练程度、边界条件的处理能力以及原地算法的设计思路。今天,我们将深入分析一个高效的原位单词反转算法,探讨其原理、优化方案以及实际应用。
目录
[1. 空字符串](#1. 空字符串)
[2. 全空格字符串](#2. 全空格字符串)
[3. 单个单词](#3. 单个单词)
[4. 连续多个空格](#4. 连续多个空格)
[1. 文本编辑器功能](#1. 文本编辑器功能)
[2. 数据处理](#2. 数据处理)
[3. 游戏开发](#3. 游戏开发)
[4. 自然语言处理](#4. 自然语言处理)
[1. 反转字符串中的单词顺序(而非单词本身)](#1. 反转字符串中的单词顺序(而非单词本身))
[2. 按照特定分隔符反转单词](#2. 按照特定分隔符反转单词)
[3. 只反转单词中的元音字母](#3. 只反转单词中的元音字母)
[1. 减少函数调用](#1. 减少函数调用)
[2. 使用局部引用](#2. 使用局部引用)
[3. 批量交换优化](#3. 批量交换优化)
[4. 内存访问优化](#4. 内存访问优化)
[1. 越界访问](#1. 越界访问)
[2. 无限循环](#2. 无限循环)
[3. 处理string::npos不正确](#3. 处理string::npos不正确)
[4. 忘记处理连续空格](#4. 忘记处理连续空格)
问题描述
给定一个字符串 s,反转字符串中每个单词的字符顺序,同时保留空格和单词的初始顺序。
注意:
-
输入字符串中可能包含前导空格、尾随空格或单词间的多个空格
-
返回的结果字符串中,空格应保留在原位置
-
单词由非空格字符组成,是字符串中的有效字符序列
示例 1:
text
输入:s = "Let's take LeetCode contest"
输出:"s'teL ekat edoCteeL tsetnoc"
示例 2:
text
输入:s = " hello world "
输出:" olleh dlrow "
算法实现
cpp
class Solution {
public:
string reverseWords(string s) {
int begin = 0;
int end = s.find(' ');
while (begin < s.size()) {
if (end == string::npos) {
end = s.size();
}
int left = begin, right = end - 1;
while (left < right) {
swap(s[left], s[right]);
left++;
right--;
}
begin = end + 1;
end = s.find(' ', begin);
}
return s;
}
};
算法详解
核心思路
-
定位单词边界 :使用
find(' ')查找空格位置,确定每个单词的结束位置 -
反转单词字符:对每个单词进行原地反转
-
移动指针:更新指针到下一个单词的起始位置
-
处理边界:正确处理最后一个单词和空格情况
算法流程图
text
输入: "Let's take LeetCode contest"
↓
初始化: begin = 0, end = 5(第一个空格位置)
↓
第一个循环:
- 反转 "Let's": "s'teL"
- begin = 6, end = 10
↓
第二个循环:
- 反转 "take": "ekat"
- begin = 11, end = 18
↓
第三个循环:
- 反转 "LeetCode": "edoCteeL"
- begin = 19, end = 26
↓
第四个循环:
- end = npos → end = s.size()
- 反转 "contest": "tsetnoc"
- begin = 27, 结束循环
↓
输出: "s'teL ekat edoCteeL tsetnoc"
关键代码分析
cpp
// 1. 查找单词边界
int end = s.find(' '); // 查找第一个空格位置
// 2. 处理最后一个单词
if (end == string::npos) {
end = s.size(); // 将npos转换为字符串长度
}
// 3. 反转单词字符
int left = begin, right = end - 1;
while (left < right) {
swap(s[left], s[right]); // 原地交换字符
left++;
right--;
}
// 4. 移动到下一个单词
begin = end + 1; // 跳过当前单词和空格
end = s.find(' ', begin); // 查找下一个空格
算法优化
优化1:使用标准库函数简化
cpp
class Solution {
public:
string reverseWords(string s) {
int start = 0;
while (start < s.size()) {
// 跳过前导空格
while (start < s.size() && s[start] == ' ') {
start++;
}
if (start >= s.size()) break;
// 找到单词的结束位置
int end = start;
while (end < s.size() && s[end] != ' ') {
end++;
}
// 反转单词
reverse(s.begin() + start, s.begin() + end);
// 移动到下一个单词
start = end;
}
return s;
}
};
优化2:双指针原地处理
cpp
class Solution {
public:
string reverseWords(string s) {
int n = s.size();
int wordStart = 0;
while (wordStart < n) {
// 跳过空格
while (wordStart < n && s[wordStart] == ' ') {
wordStart++;
}
if (wordStart >= n) break;
// 找到单词的结束
int wordEnd = wordStart;
while (wordEnd < n && s[wordEnd] != ' ') {
wordEnd++;
}
// 反转单词
int left = wordStart, right = wordEnd - 1;
while (left < right) {
swap(s[left], s[right]);
left++;
right--;
}
// 移动到下一个单词
wordStart = wordEnd;
}
return s;
}
};
优化3:一次遍历完成所有操作
cpp
class Solution {
public:
string reverseWords(string s) {
int n = s.size();
for (int i = 0; i < n; i++) {
if (s[i] != ' ') {
// 找到单词的起始位置
int j = i;
// 找到单词的结束位置
while (j < n && s[j] != ' ') {
j++;
}
// 反转单词
reverse(s.begin() + i, s.begin() + j);
// 更新i的位置
i = j - 1;
}
}
return s;
}
};
时间复杂度与空间复杂度
原始算法
-
时间复杂度:O(n),其中n是字符串长度。每个字符最多被访问两次(一次在查找边界时,一次在反转时)
-
空间复杂度:O(1),原地修改,只使用常数个额外变量
各种优化方法的对比
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 原始方法 | O(n) | O(1) | 思路清晰 | 多次调用find函数 |
| 标准库优化 | O(n) | O(1) | 代码简洁 | 依赖标准库 |
| 双指针法 | O(n) | O(1) | 一次遍历 | 需要处理边界 |
| 一次遍历法 | O(n) | O(1) | 效率高 | 逻辑稍复杂 |
边界条件处理
1. 空字符串
cpp
if (s.empty()) return s;
2. 全空格字符串
cpp
// 算法需要正确处理这种情况
// 原始算法中,当begin < s.size()时进入循环,但每次反转空区间
3. 单个单词
cpp
// 没有空格的情况,end = npos,正确处理
4. 连续多个空格
cpp
// 原始算法会将连续空格之间的空字符串视为"单词"进行反转
// 反转空区间不会改变字符串
测试用例
cpp
void testReverseWords() {
Solution solution;
// 标准测试
assert(solution.reverseWords("Let's take LeetCode contest") == "s'teL ekat edoCteeL tsetnoc");
// 边界测试
assert(solution.reverseWords("") == "");
assert(solution.reverseWords(" ") == " ");
assert(solution.reverseWords(" ") == " ");
// 单个单词
assert(solution.reverseWords("hello") == "olleh");
assert(solution.reverseWords("a") == "a");
// 多个空格
assert(solution.reverseWords(" hello world ") == " olleh dlrow ");
assert(solution.reverseWords("a b c") == "a b c");
// 混合测试
assert(solution.reverseWords("the sky is blue") == "eht yks si eulb");
assert(solution.reverseWords(" Bob Loves Alice ") == " boB sevoL ecilA ");
cout << "所有测试通过!" << endl;
}
实际应用场景
1. 文本编辑器功能
-
实现"反转选定文本"功能
-
文本加密/解密工具
-
代码格式化工具
2. 数据处理
-
日志文件处理
-
数据库查询结果格式化
-
数据清洗和转换
3. 游戏开发
-
文字谜题游戏
-
密码生成器
-
文本特效生成
4. 自然语言处理
-
文本预处理
-
特征提取
-
数据增强(通过文本变换生成训练数据)
扩展问题
1. 反转字符串中的单词顺序(而非单词本身)
cpp
class Solution {
public:
string reverseWordsOrder(string s) {
// 移除多余空格
string cleaned;
int n = s.size();
for (int i = 0; i < n; i++) {
if (s[i] != ' ') {
cleaned += s[i];
} else if (!cleaned.empty() && cleaned.back() != ' ') {
cleaned += ' ';
}
}
// 移除末尾可能的多余空格
if (!cleaned.empty() && cleaned.back() == ' ') {
cleaned.pop_back();
}
// 反转整个字符串
reverse(cleaned.begin(), cleaned.end());
// 反转每个单词
int start = 0;
for (int i = 0; i <= cleaned.size(); i++) {
if (i == cleaned.size() || cleaned[i] == ' ') {
reverse(cleaned.begin() + start, cleaned.begin() + i);
start = i + 1;
}
}
return cleaned;
}
};
2. 按照特定分隔符反转单词
cpp
class Solution {
public:
string reverseWordsWithDelimiter(string s, char delimiter) {
int start = 0;
while (start < s.size()) {
// 找到下一个分隔符
int end = start;
while (end < s.size() && s[end] != delimiter) {
end++;
}
// 反转单词
reverse(s.begin() + start, s.begin() + end);
// 移动到下一个单词
start = end + 1;
}
return s;
}
};
3. 只反转单词中的元音字母
cpp
class Solution {
public:
string reverseVowelsInWords(string s) {
auto isVowel = [](char c) {
c = tolower(c);
return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
};
int start = 0;
while (start < s.size()) {
// 跳过非字母字符
while (start < s.size() && !isalpha(s[start])) {
start++;
}
if (start >= s.size()) break;
// 找到单词结束位置
int end = start;
while (end < s.size() && isalpha(s[end])) {
end++;
}
// 反转单词中的元音字母
int left = start, right = end - 1;
while (left < right) {
// 找到左边的元音
while (left < right && !isVowel(s[left])) left++;
// 找到右边的元音
while (left < right && !isVowel(s[right])) right--;
if (left < right) {
swap(s[left], s[right]);
left++;
right--;
}
}
start = end;
}
return s;
}
};
性能优化技巧
1. 减少函数调用
cpp
// 原始代码多次调用s.find(' ')
// 可以改为使用指针遍历,减少函数调用开销
2. 使用局部引用
cpp
string reverseWords(string s) {
char* str = &s[0]; // 获取原始指针,减少operator[]的开销
int n = s.size();
int start = 0;
while (start < n) {
// ... 使用str指针操作
}
return s;
}
3. 批量交换优化
cpp
// 对于较长的单词,可以使用更高效的交换方式
void reverseSegment(string& s, int left, int right) {
while (left < right) {
// 使用异或交换,减少临时变量
s[left] ^= s[right];
s[right] ^= s[left];
s[left] ^= s[right];
left++;
right--;
}
}
4. 内存访问优化
cpp
// 考虑CPU缓存行,优化内存访问模式
string reverseWords(string s) {
int n = s.size();
const int CACHE_LINE = 64; // 典型缓存行大小
for (int i = 0; i < n; i += CACHE_LINE) {
int block_end = min(i + CACHE_LINE, n);
// 处理一个缓存行内的单词
// ... 具体实现
}
return s;
}
常见错误与注意事项
1. 越界访问
cpp
// 错误示例
int right = end - 1; // 当end=0时,right=-1,访问越界
// 正确做法:确保索引有效
if (end > 0) {
int right = end - 1;
// ... 反转操作
}
2. 无限循环
cpp
// 错误示例:忘记更新指针
while (begin < s.size()) {
// 反转操作...
// 缺少 begin = end + 1; 导致无限循环
}
3. 处理string::npos不正确
cpp
// 错误示例:直接使用npos进行计算
int length = end - begin; // 如果end是npos,计算错误
// 正确做法:先检查是否为npos
if (end == string::npos) {
end = s.size();
}
4. 忘记处理连续空格
cpp
// 错误示例:假设单词间只有一个空格
begin = end + 1; // 如果连续多个空格,可能跳过单词
// 正确做法:跳过所有空格
while (begin < s.size() && s[begin] == ' ') {
begin++;
}
总结
单词反转算法是一个看似简单但蕴含深度的字符串处理问题。通过分析这个算法,我们学习了:
-
字符串遍历技巧:如何高效地定位和操作子串
-
边界条件处理:空字符串、空格、单个字符等特殊情况
-
原地算法设计:如何在有限空间内完成复杂操作
-
性能优化:减少函数调用、优化内存访问等技巧
关键要点:
-
使用双指针技术高效定位单词边界
-
原地交换避免额外空间开销
-
正确处理各种边界情况和异常输入
-
根据实际需求选择合适的优化策略
掌握这个算法不仅有助于解决类似问题,还能提高对字符串处理、算法设计和性能优化的整体理解。在实际开发中,这种"分而治之"的字符串处理思想可以应用于许多其他场景,如文本解析、数据清洗、模式匹配等。
记住:优秀的字符串算法不仅在于正确性,还在于对边缘情况的全面考虑、代码的可读性和执行效率。通过不断练习和思考这些基础算法,我们可以培养出解决复杂问题的能力。