ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LeetCode 151:双指针法反转字符串单词详解

LeetCode 151:双指针法反转字符串单词详解

1. 题目背景与核心需求解析

这道LeetCode 151题"反转字符串中的单词"是字符串处理类问题的经典代表,也是大厂面试中的高频考题。题目要求我们将给定字符串中的单词顺序进行反转,同时需要处理多余空格的问题。举个例子:

输入:"the sky is blue" 输出:"blue is sky the"

看似简单的需求背后,隐藏着几个关键考察点:

  1. 字符串中可能存在前导/尾随空格
  2. 单词间可能存在多个连续空格
  3. 需要保证反转后的字符串单词间只保留单个空格
  4. 不能使用语言内置的split()等高级函数(面试时常见限制)

1.1 大厂面试的深层考察意图

这道题在大厂机考中频繁出现,主要考察以下几个维度:

  • 对字符串基本操作的掌握程度(遍历、截取、拼接等)
  • 边界条件的处理能力(空格处理、空字符串等)
  • 空间复杂度的控制意识(是否使用O(1)空间解法)
  • 代码实现的简洁性与可读性

在实际面试场景中,面试官往往会追加限制条件,比如: "请在不使用额外空间的情况下完成" "请用O(n)时间复杂度和O(1)空间复杂度实现" 这些变体都需要我们对基础解法有深入理解才能应对。

2. 基础解法:双端队列+双指针法详解

2.1 算法思路拆解

双端队列+双指针法是解决这类字符串反转问题的经典套路,其核心思想可以分解为以下步骤:

  1. 预处理阶段:使用双指针跳过字符串首尾的空格
  2. 单词提取阶段:使用快慢指针配合,识别并提取完整单词
  3. 存储阶段:将提取的单词按顺序存入双端队列的头部
  4. 拼接阶段:将队列中的单词用空格连接输出

这种方法的优势在于:

  • 时间复杂度O(n):只需一次完整遍历
  • 空间复杂度O(n):需要额外存储单词(符合大多数面试要求)
  • 逻辑清晰:各步骤职责明确,易于理解和实现

2.2 完整代码实现与逐行解析

from collections import deque def reverseWords(s: str) -> str: left, right = 0, len(s) - 1 # 去除开头空格 while left <= right and s[left] == ' ': left += 1 # 去除末尾空格 while left <= right and s[right] == ' ': right -= 1 dq = deque() word = [] # 提取单词 while left <= right: if s[left] != ' ': word.append(s[left]) else: if word: # 遇到空格且word不为空,表示一个单词结束 dq.appendleft(''.join(word)) word = [] left += 1 # 添加最后一个单词 dq.appendleft(''.join(word)) return ' '.join(dq)

关键代码解析:

  1. left和right指针初始定位到字符串首尾非空格位置
  2. word列表动态收集当前单词的字符
  3. 遇到空格时,将完整单词添加到双端队列头部(实现反转)
  4. 最后处理末尾单词(因为末尾没有触发空格的条件)

2.3 复杂度分析与优化空间

时间复杂度:O(n)

  • 每个字符最多被访问两次(一次跳过空格,一次处理单词)
  • 双端队列的appendleft操作是O(1)

空间复杂度:O(n)

  • 存储所有单词需要额外空间
  • 单词列表和双端队列的空间消耗

优化方向:

  1. 如果允许使用split(),代码可以更简洁(但面试常限制使用)
  2. 可以使用字符串拼接代替双端队列(但时间复杂度可能上升)
  3. 更高级的O(1)空间解法需要原地修改字符串(Python中字符串不可变,较难实现)

3. 核心技巧与避坑指南

3.1 双指针使用的五个关键细节

  1. 指针移动顺序:必须先移动右指针跳过尾部空格,再处理左指针
  2. 边界条件检查:while循环必须包含left <= right条件,防止越界
  3. 单词结束判断:遇到空格且word不为空时才视为单词结束
  4. 最后一个单词处理:循环结束后必须单独处理最后一个单词
  5. 连续空格处理:通过if word条件过滤掉连续空格产生的影响

3.2 面试中常见的三个实现陷阱

  1. 忘记处理首尾空格:直接开始处理会导致结果包含多余空格

    • 错误示例:输入" hello world "输出"world hello"
  2. 连续空格处理不当:可能导致结果中出现空单词

    • 错误示例:输入"a b"输出"b a"
  3. 最后一个单词遗漏:因为末尾没有触发空格条件而忘记添加

    • 错误示例:输入"hello world"输出"world"

3.3 调试与验证方法

建议使用以下测试用例验证代码正确性:

测试用例: 1. 常规情况:"the sky is blue" → "blue is sky the" 2. 前后空格:" hello world " → "world hello" 3. 连续空格:"a good example" → "example good a" 4. 单单词:"hello" → "hello" 5. 全空格:" " → ""

在IDE中调试时可以:

  1. 在指针移动处打印left/right值
  2. 在添加单词时打印当前双端队列状态
  3. 使用断点观察word列表的变化过程

4. 解法变体与进阶思路

4.1 不使用双端队列的替代方案

如果面试中限制使用双端队列,可以用列表+反转代替:

def reverseWords(s: str) -> str: # ... 前处理相同 ... words = [] word = [] while left <= right: if s[left] != ' ': word.append(s[left]) else: if word: words.append(''.join(word)) word = [] left += 1 words.append(''.join(word)) return ' '.join(reversed(words))

4.2 O(1)空间复杂度的挑战

对于支持字符串可变性的语言(如C++),可以实现真正的O(1)空间解法:

  1. 先反转整个字符串
  2. 再反转每个单词
  3. 最后处理多余空格

Python中由于字符串不可变,难以实现真正的O(1)空间,但可以模拟:

def reverseWords(s: str) -> str: # 转换为列表模拟可变字符串 s = list(s) n = len(s) # 反转整个字符串 def reverse(l, r): while l < r: s[l], s[r] = s[r], s[l] l += 1 r -= 1 # 反转每个单词 def reverse_each_word(): start = end = 0 while start < n: # 跳过空格 while start < n and s[start] == ' ': start += 1 end = start # 定位单词结尾 while end < n and s[end] != ' ': end += 1 reverse(start, end - 1) start = end # 去除多余空格 def trim_spaces(): slow = fast = 0 # 跳过开头空格 while fast < n and s[fast] == ' ': fast += 1 # 处理中间部分 while fast < n: if s[fast] != ' ': s[slow] = s[fast] slow += 1 fast += 1 else: # 保留一个空格 s[slow] = ' ' slow += 1 fast += 1 # 跳过后续空格 while fast < n and s[fast] == ' ': fast += 1 # 处理末尾可能多余的空格 return ''.join(s[:slow-1] if slow > 0 and s[slow-1] == ' ' else s[:slow]) reverse(0, n-1) reverse_each_word() return trim_spaces()

4.3 实际工程中的优化考量

在真实项目场景中,我们还需要考虑:

  1. 多语言支持:处理Unicode字符时的特殊考虑
  2. 超大字符串处理:内存受限时的流式处理方案
  3. 性能优化:对于频繁调用的场景,可以预编译正则表达式
  4. API设计:是否提供原地修改和返回新字符串两种选项

5. 同类问题拓展与练习建议

5.1 推荐练习的变体题目

  1. LeetCode 186. 反转字符串中的单词 II(要求原地修改)
  2. LeetCode 557. 反转字符串中的单词 III(反转每个单词但保持顺序)
  3. LeetCode 344. 反转字符串(更基础的反转问题)
  4. LeetCode 541. 反转字符串 II(每隔2k字符反转前k个)

5.2 字符串处理通用技巧总结

  1. 双指针法:适用于大多数遍历+条件处理场景
  2. 滑动窗口:解决子串/子数组相关问题
  3. 递归回溯:处理组合类字符串问题
  4. KMP算法:高效解决字符串匹配问题
  5. 字典树(Trie):处理前缀相关查询

5.3 大厂面试准备建议

  1. 至少掌握3种不同的实现方法
  2. 能够分析各方法的时间/空间复杂度
  3. 准备测试用例覆盖各种边界条件
  4. 练习在白板/记事本上手写代码
  5. 模拟面试时限制使用特定函数(如不用split())

我在实际面试辅导中发现,很多同学卡在这道题不是因为算法不会,而是输在细节处理上。建议每次写完代码后,用这几个问题自检:

  1. 是否处理了首尾空格?
  2. 连续空格情况下是否正确输出?
  3. 单单词和全空格的特殊情况能否处理?
  4. 代码是否清晰易读,变量命名是否合理?
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进