1. 算法训练营第四天核心任务解析
今天我们要啃下两块硬骨头:字符串翻转和子串查找。151题"翻转字符串里的单词"看似简单,实则暗藏多个考察点;28题"实现strStr()"则是经典的字符串匹配问题,KMP算法在这里大显身手。作为训练营第四天的内容,这两题完美衔接了前三天的基础知识,开始向中等难度过渡。
我清楚地记得第一次做151题时,以为直接用split+reverse就能搞定,结果被各种边界条件教做人。而strStr()更是让我在KMP算法上栽了跟头,直到画了十几遍状态转移图才真正开窍。下面我就把这两道题的解题心得分步骤拆解,特别是那些容易踩坑的细节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 151.翻转字符串里的单词:从暴力到优雅
2.1 题目本质与常见误区
给定一个字符串s,需要反转字符串中单词的顺序,同时去除多余空格。示例:
输入:" hello world "
输出:"world hello"
新手容易犯的三个典型错误:
- 直接整体反转字符串(得到"dlrow olleh")
- 用split()后不做空格处理(保留多个空格)
- 忘记处理首尾空格
关键理解:这题考察的是字符串操作的综合能力,包括:空格处理、单词识别、顺序反转、字符串拼接
2.2 标准解法四步走
解法一:语言特性法(Python示例)
python复制def reverseWords(s: str) -> str:
return ' '.join(reversed(s.split()))
虽然简洁,但面试时这样写可能拿不到满分,因为没展示出底层实现逻辑。
解法二:手动实现版(更受面试官青睐)
python复制def reverseWords(s: str) -> str:
# 1. 去除首尾空格
left, right = 0, len(s) - 1
while left <= right and s[left] == ' ':
left += 1
while left <= right and s[right] == ' ':
right -= 1
# 2. 收集单词
word, words = [], []
while left <= right:
if s[left] != ' ':
word.append(s[left])
elif word: # 遇到空格且word不为空
words.append(''.join(word))
word = []
left += 1
words.append(''.join(word)) # 添加最后一个单词
# 3. 反转单词列表
words.reverse()
# 4. 拼接结果
return ' '.join(words)
时间复杂度:O(n) 空间复杂度:O(n)
2.3 双指针优化方案
为了达到O(1)空间复杂度(假设字符串可变),可以使用更复杂的双指针原地修改:
- 整体反转字符串
- 逐个单词反转
- 去除多余空格
但实际面试中,除非特别要求,否则解法二已经足够展示能力。我在LeetCode实测发现,Python中解法一反而运行最快,因为内置方法经过高度优化。
3. 28.实现strStr():从暴力到KMP
3.1 题目理解与暴力解法
实现字符串查找函数,返回needle在haystack中首次出现的索引,不存在则返回-1。
暴力解法(双指针):
python复制def strStr(haystack: str, needle: str) -> int:
L, n = len(needle), len(haystack)
for start in range(n - L + 1):
if haystack[start:start + L] == needle:
return start
return -1
时间复杂度:O((n-L)*L) 最坏情况下接近O(n^2)
3.2 KMP算法深度解析
KMP算法的核心是通过预处理模式串(needle)构建部分匹配表(PMT),将时间复杂度降为O(n+m)。
部分匹配表构建原理:
对于模式串"aabaaf",其PMT为[0,1,0,1,2,0]
- PMT[i]表示子串pattern[0:i+1]的最长相同前后缀长度
- 构建时使用双指针技巧,类似动态规划
KMP实现代码:
python复制def strStr(haystack: str, needle: str) -> int:
def build_pmt(pattern):
pmt = [0] * len(pattern)
j = 0
for i in range(1, len(pattern)):
while j > 0 and pattern[i] != pattern[j]:
j = pmt[j-1]
if pattern[i] == pattern[j]:
j += 1
pmt[i] = j
return pmt
if not needle:
return 0
pmt = build_pmt(needle)
j = 0
for i in range(len(haystack)):
while j > 0 and haystack[i] != needle[j]:
j = pmt[j-1]
if haystack[i] == needle[j]:
j += 1
if j == len(needle):
return i - j + 1
return -1
3.3 KMP算法常见误区
- 错误理解next数组含义(next数组是PMT的右移版本)
- 忘记处理空字符串情况
- 主循环中指针回退逻辑错误
- 构建PMT时初始化不当
我在第三次刷这道题时才真正理解:当字符不匹配时,j = pmt[j-1]这个操作实际上是在利用已匹配部分的最大相同前后缀信息,避免完全回溯。
4. 实战中的优化技巧
4.1 字符串翻转的工程实践
在实际项目中处理字符串时:
- 优先考虑使用正则处理复杂空格情况
python复制import re
def reverseWords(s: str) -> str:
return ' '.join(reversed(re.sub(r'\s+', ' ', s.strip()).split()))
- 处理超长字符串时考虑生成器表达式
- 注意不同语言字符串可变性差异(Python中字符串不可变)
4.2 字符串搜索算法选择
根据场景选择合适算法:
- 短文本:暴力法足够
- 多次搜索同一模式:预计算PMT复用
- 大规模文本:Boyer-Moore算法(坏字符规则)
- 多模式匹配:Trie树或AC自动机
4.3 调试技巧
对于KMP算法:
- 打印PMT构建过程
- 可视化匹配过程(箭头标注指针位置)
- 用简单测试用例逐步验证:
- 空字符串
- 完全匹配
- 部分匹配
- 完全不匹配
我在白板练习时发现,用"aabaabaaf"匹配"aabaaf"这个例子能很好演示KMP的优势——当f不匹配时,j从5回退到2而不是0。
