【C++】单词反转算法详解:原地操作与边界处理

引言

字符串中的单词反转是一个经典问题,它考察了对字符串操作的熟练程度、边界条件的处理能力以及原地算法的设计思路。今天,我们将深入分析一个高效的原位单词反转算法,探讨其原理、优化方案以及实际应用。

目录

引言

问题描述

算法实现

算法详解

核心思路

算法流程图

关键代码分析

算法优化

优化1:使用标准库函数简化

优化2:双指针原地处理

优化3:一次遍历完成所有操作

时间复杂度与空间复杂度

原始算法

各种优化方法的对比

边界条件处理

[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;
    }
};

算法详解

核心思路

  1. 定位单词边界 :使用 find(' ') 查找空格位置,确定每个单词的结束位置

  2. 反转单词字符:对每个单词进行原地反转

  3. 移动指针:更新指针到下一个单词的起始位置

  4. 处理边界:正确处理最后一个单词和空格情况

算法流程图

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++;
}

总结

单词反转算法是一个看似简单但蕴含深度的字符串处理问题。通过分析这个算法,我们学习了:

  1. 字符串遍历技巧:如何高效地定位和操作子串

  2. 边界条件处理:空字符串、空格、单个字符等特殊情况

  3. 原地算法设计:如何在有限空间内完成复杂操作

  4. 性能优化:减少函数调用、优化内存访问等技巧

关键要点

  • 使用双指针技术高效定位单词边界

  • 原地交换避免额外空间开销

  • 正确处理各种边界情况和异常输入

  • 根据实际需求选择合适的优化策略

掌握这个算法不仅有助于解决类似问题,还能提高对字符串处理、算法设计和性能优化的整体理解。在实际开发中,这种"分而治之"的字符串处理思想可以应用于许多其他场景,如文本解析、数据清洗、模式匹配等。

记住:优秀的字符串算法不仅在于正确性,还在于对边缘情况的全面考虑、代码的可读性和执行效率。通过不断练习和思考这些基础算法,我们可以培养出解决复杂问题的能力。

相关推荐
senijusene1 小时前
通信概念,51UART的使用,以及MODBUS的简单应用
c语言·开发语言·单片机·51单片机
wyiyiyi1 小时前
【线性代数】对偶空间与矩阵转置及矩阵分解(Java讲解)
java·线性代数·支持向量机·矩阵·数据分析
老星*2 小时前
Vaultwarden:开源轻量的1Password替代,自托管密码管理方案
开源·github·密码学
你这个代码我看不懂2 小时前
磁盘的存储原理
java
吗~喽2 小时前
【C++】模板的两大特性
c++
饥饿的帕尼尼2 小时前
Claude Code本地安装使用教程
node.js·github·claude
泯泷2 小时前
从零构建寄存器式 JSVMP:实战教程导读
前端·javascript·算法
NGC_66112 小时前
值传递和引用传递辨析
算法
王璐WL2 小时前
【C++】string类基础知识
开发语言·c++