1. 问题背景与核心挑战
LeetCode第30题"串联所有单词的子串"是字符串处理类题目中的经典难题。给定一个字符串s和一个字符串数组words,要求找出s中所有恰好由words中所有单词串联形成的子串的起始索引。words中的单词长度相同且可能重复。
这个问题的难点在于:
- 需要同时满足所有单词的完整出现(包括重复单词)
- 单词可以以任意顺序排列
- 子串必须严格连续且不包含其他字符
- 输入规模可能很大(s长度可达10^4,words长度可达5000)
2. 解题思路分析
2.1 暴力解法及其缺陷
最直观的解法是:
- 生成words所有可能的排列组合
- 在s中搜索每个组合的出现位置
但这种方法时间复杂度为O(N!×M),其中N是words长度,M是s长度。当N=10时,10! = 3628800,完全不可行。
2.2 滑动窗口+哈希计数的优势
更优的解法结合了滑动窗口和哈希计数:
- 利用所有单词长度相同的特点(设为word_len)
- 将问题转化为在s中寻找长度为word_len×words_num的子串
- 使用哈希表记录words中每个单词的出现次数
- 滑动窗口检查每个候选子串是否符合要求
这种方法将时间复杂度降为O(N×M),空间复杂度O(N)。
3. 详细实现步骤
3.1 预处理阶段
from collections import defaultdict def findSubstring(s, words): if not s or not words: return [] word_len = len(words[0]) total_len = word_len * len(words) word_count = defaultdict(int) for word in words: word_count[word] += 1关键点:
- 先处理边界情况(空输入)
- 计算单个单词长度和总子串长度
- 使用defaultdict统计每个单词出现次数
3.2 滑动窗口实现
result = [] n = len(s) for i in range(word_len): left = i curr_count = defaultdict(int) count = 0 for j in range(i, n - word_len + 1, word_len): word = s[j:j+word_len] if word in word_count: curr_count[word] += 1 count += 1 while curr_count[word] > word_count[word]: left_word = s[left:left+word_len] curr_count[left_word] -= 1 left += word_len count -= 1 if count == len(words): result.append(left) left_word = s[left:left+word_len] curr_count[left_word] -= 1 left += word_len count -= 1 else: curr_count.clear() count = 0 left = j + word_len return result代码解析:
- 外层循环处理不同起始位置(0到word_len-1)
- 维护窗口[left, j]和当前计数curr_count
- 当发现不在words中的单词时,重置窗口
- 当某个单词超额时,移动左边界直到合规
- 当count等于words长度时,记录有效索引
4. 关键优化技巧
4.1 窗口移动的步长优化
由于所有单词长度相同,窗口可以以word_len为步长移动,而不是逐字符移动。这使得时间复杂度从O(M×N)降为O(M×word_len)。
4.2 哈希表的快速比对
使用两个哈希表:
- word_count:记录words的标准分布
- curr_count:记录当前窗口的实际分布
通过比较两个哈希表是否相同来判断窗口有效性,比字符串拼接比较效率高得多。
4.3 提前终止条件
当剩余字符串长度不足total_len时,可以提前终止搜索,避免无意义的检查。
5. 边界情况处理
5.1 输入为空的情况
if not s or not words: return []5.2 单词长度不一致
题目已保证words中所有单词长度相同,但实际工程中需要验证:
if any(len(w) != word_len for w in words): return []5.3 超长输入处理
对于特别长的s和words,可以考虑:
- 先检查s长度是否足够
- 使用更高效的数据结构(如原生dict代替defaultdict)
6. 复杂度分析
时间复杂度:O(word_len × n)。外层循环word_len次,内层最多处理n/word_len次。
空间复杂度:O(m)。需要存储words的哈希表,m为words中不同单词的数量。
7. 实际测试案例
测试用例1:
s = "barfoothefoobarman" words = ["foo","bar"] # 输出:[0,9]测试用例2:
s = "wordgoodgoodgoodbestword" words = ["word","good","best","word"] # 输出:[]测试用例3:
s = "a" * 10000 words = ["a"] * 5000 # 需要高效处理大规模输入8. 常见错误与调试技巧
8.1 窗口边界错误
典型错误:没有正确处理窗口左边界移动,导致遗漏或重复计数。
调试方法:
- 打印窗口变化时的left, j和curr_count
- 使用小测试用例逐步跟踪
8.2 哈希表比对错误
常见问题:直接比较两个defaultdict对象可能不如预期。
解决方案:
- 转换为普通dict再比较
- 或者逐个键值比较
8.3 性能优化技巧
当words有很多重复单词时:
- 可以先统计unique单词
- 对s预处理建立单词位置索引
- 使用位图等压缩存储方式
9. 算法扩展思考
9.1 变体问题:单词长度不同
如果words中单词长度不同,问题会更复杂。可能的解法:
- 使用回溯法尝试所有组合
- 结合Trie树进行模式匹配
9.2 实际应用场景
这种算法可用于:
- DNA序列模式查找
- 文档内容指纹识别
- 网络流量特征检测
10. 个人实现心得
在实际编码中发现几个关键点:
- 窗口移动时要同时更新计数和窗口大小
- Python的defaultdict在清空时最好重新初始化,避免残留数据
- 对于超长重复字符串,可以先检查首字符是否匹配再进行完整比较
- 在竞赛中,可以预先计算所有可能的单词哈希值加速比较
一个容易忽略的优化:当words中存在重复单词时,可以先对words排序,然后在滑动窗口中对截取的单词也排序后比较,虽然增加了排序开销,但减少了哈希表操作。