1. 算法训练营第四天核心内容解析
今天我们要啃下两块硬骨头:字符串翻转和子串查找。作为代码随想录算法训练营的第四天内容,这两道题目看似基础,却暗藏玄机。151题"翻转字符串里的单词"考察的是对字符串操作的全面掌握,而28题"实现strStr()"则是经典的KMP算法应用场景。
我清楚地记得第一次做这两道题时的窘境——翻转字符串时总有几个空格处理不好,KMP算法看了一遍又一遍还是似懂非懂。经过反复练习和总结,现在终于能够游刃有余地解决这类问题。下面就把我的解题心得完整分享给大家,特别是那些容易踩坑的细节。
2. 151.翻转字符串里的单词详解
2.1 问题本质与解题思路
题目要求翻转字符串中单词的顺序,同时去除多余空格。例如:
输入:" hello world "
输出:"world hello"
这个问题的难点不在于算法本身,而在于对字符串处理的全面考虑。很多同学(包括当初的我)容易犯以下错误:
- 没有处理前导/后缀空格
- 单词间多个空格只保留一个的处理不到位
- 翻转逻辑写得太复杂
最优解法分三步走:
- 去除多余空格(前导、后缀、中间多余空格)
- 翻转整个字符串
- 翻转每个单词
2.2 代码实现与关键点
python复制def reverseWords(s: str) -> str:
# 1. 去除多余空格
def trim_spaces(s):
left, right = 0, len(s) - 1
# 去掉前后空格
while left <= right and s[left] == ' ':
left += 1
while left <= right and s[right] == ' ':
right -= 1
# 去掉中间多余空格
output = []
while left <= right:
if s[left] != ' ':
output.append(s[left])
elif output[-1] != ' ': # 当前是空格但前一个不是空格才添加
output.append(s[left])
left += 1
return output
# 2. 翻转字符数组
def reverse(l, left, right):
while left < right:
l[left], l[right] = l[right], l[left]
left += 1
right -= 1
# 3. 翻转每个单词
def reverse_each_word(l):
n = len(l)
start = end = 0
while start < n:
while end < n and l[end] != ' ':
end += 1
reverse(l, start, end - 1)
start = end + 1
end += 1
l = trim_spaces(s)
reverse(l, 0, len(l) - 1)
reverse_each_word(l)
return ''.join(l)
关键技巧:先处理空格再翻转,比边翻转边处理空格要简单得多。trim_spaces函数中的双指针法是处理字符串空格的经典模式。
2.3 复杂度分析与变种
时间复杂度:O(n),所有操作都是线性扫描
空间复杂度:O(n),Python中字符串不可变需要额外空间
常见变种题目:
- 保持原始空格数量不变翻转单词
- 每隔k个单词翻转一次
- 处理包含标点符号的情况
3. 28.实现strStr()深度剖析
3.1 问题理解与暴力解法
strStr()即C语言中的字符串查找函数,要求返回needle在haystack中第一次出现的位置。例如:
输入:haystack = "hello", needle = "ll"
输出:2
暴力解法思路简单:
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),当needle很长时效率低下
3.2 KMP算法精讲
KMP算法的核心是利用部分匹配表(PMT)避免不必要的回溯。理解KMP需要掌握:
- 前缀和后缀的定义
- 部分匹配值的计算
- next数组的构建
next数组构建代码:
python复制def build_next(needle):
next = [0] * len(needle)
j = 0
for i in range(1, len(needle)):
while j > 0 and needle[i] != needle[j]:
j = next[j - 1]
if needle[i] == needle[j]:
j += 1
next[i] = j
return next
完整的KMP实现:
python复制def strStr(haystack: str, needle: str) -> int:
if not needle:
return 0
next = build_next(needle)
j = 0
for i in range(len(haystack)):
while j > 0 and haystack[i] != needle[j]:
j = next[j - 1]
if haystack[i] == needle[j]:
j += 1
if j == len(needle):
return i - j + 1
return -1
3.3 KMP算法复杂度与实战技巧
时间复杂度:O(m+n),其中m是needle长度,n是haystack长度
空间复杂度:O(m),用于存储next数组
实战经验:当needle长度超过5时,KMP的优势开始显现。对于特别长的模式串,可以考虑Boyer-Moore等更高效的算法。
4. 常见问题与调试技巧
4.1 翻转字符串常见bug
-
翻转后单词内部顺序也被翻转了
- 原因:忘记在整体翻转后对每个单词单独翻转
- 修复:确保执行第三步reverse_each_word
-
空格处理不正确
- 测试用例:"a good example"
- 预期结果:"example good a"
- 错误表现:"example good a"
- 修复:仔细检查trim_spaces逻辑
4.2 KMP算法调试要点
-
next数组计算错误
- 验证方法:手工计算几个简单case的next数组
- 例如:"aabaaf"的next应为[0,1,0,1,2,0]
-
指针回溯逻辑错误
- 典型症状:部分匹配但无法找到完整匹配
- 调试方法:打印每次不匹配时的j值变化
4.3 性能优化建议
-
对于翻转字符串问题:
- 在C++等语言中可原地操作,空间复杂度可降至O(1)
- 使用字符串流可以简化空格处理
-
对于strStr问题:
- 实际工程中直接调用语言内置方法
- 面试时需要手写时,先说明暴力解法再优化
5. 扩展练习与学习路线
为了真正掌握这两类问题,建议完成以下扩展练习:
-
字符串翻转系列:
- 557.反转字符串中的单词III
- 186.翻转字符串里的单词II(会员题)
- 剑指Offer58-II.左旋转字符串
-
字符串匹配系列:
- 214.最短回文串(hard)
- 459.重复的子字符串
- 1392.最长快乐前缀
学习算法时,我的个人经验是:
- 先理解暴力解法,再学习优化算法
- 手工走几个测试用例比直接看代码更有效
- 每周复习一次做过的题目,防止遗忘
对于KMP这样的复杂算法,不要指望一次就能完全掌握。我前后反复学习了三遍,每次都有新的理解。现在遇到字符串匹配问题时,第一反应就是分析是否可以用KMP优化。
