627 字
3 分钟
30. 串联所有单词的子串
30. 串联所有单词的子串
题目描述
给定一个字符串 s 和一个字符串数组 words,其中 words 中所有字符串长度相同。在 s 中找到所有子串的起始索引,这些子串是 words 中每个单词恰好一次、无间隔地串联而成的排列。
示例 :
输入:s = "barfoothefoobarman", words = ["foo","bar"]输出:[0,9](子串 [0,6)="barfoo",[9,15)="foobar")解法思路
核心思想:把每个单词当作一个”超级字符”,问题就退化成了 438 的异位词滑窗。
由于所有单词等长 word_len,正确的串联子串一定从某一个 offset(0..word_len-1)开始,且该子串按 word_len 切分后,得到的单词序列必须与 words 的计数一致。因此分 word_len 组分别做滑窗:
- 对每个
offset,把s按word_len切成单词序列w; - 维护窗口内单词计数字典
cur和一个valid变量,记录频次恰好匹配的单词种类数; - 入单词:
cur[w]++,若正好等于target[w],valid+1; - 出单词(窗口总长超过
total_len = len(words)*word_len时):cur[w_left]--,若先前正好匹配则valid-1; - 遇到不在
target中的单词,直接清空窗口重置; - 当
valid == len(target),当前起点left就是一个合法子串起点。
逐步举例(s="barfoothefoobarman", words=["foo","bar"],word_len=3, total_len=6,只看 offset=0 分组):
- 切分出的单词序列:
bar foo the foo bar man。 - 入
"bar":cur[bar]=1==target[bar],valid=1。 - 入
"foo":cur[foo]=1==target[foo],valid=2==len(target)→append(0)✓。 - 入
"the"(不在 target)→ 清空窗口,left跳到其后。 - 最终
offset=0得到起点0;offset=3分组还会得到起点9。
复杂度分析
- 时间复杂度:O(n·word_len),每组各做一次 O(n/word_len) 遍历。
- 空间复杂度:O(n),哈希表存储单词计数。
代码实现
class Solution: def findSubstring(self, s: str, words: list[str]) -> list[int]: word_len = len(words[0]) word_count = len(words) total_len = word_len * word_count n = len(s) if n < total_len: return []
from collections import Counter target = Counter(words) ans = []
for offset in range(word_len): left = offset cur = {} valid = 0
for right in range(offset, n - word_len + 1, word_len): w = s[right:right + word_len]
if w not in target: cur.clear() valid = 0 left = right + word_len continue
cur[w] = cur.get(w, 0) + 1 if cur[w] == target[w]: valid += 1
while right - left + word_len > total_len: w_left = s[left:left + word_len] if cur[w_left] == target[w_left]: valid -= 1 cur[w_left] -= 1 left += word_len
if valid == len(target): ans.append(left)
return ansimport sysimport unittestfrom pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))from solution import Solution
class TestFindSubstring(unittest.TestCase): def setUp(self): self.solution = Solution()
def test_example1(self): self.assertEqual( self.solution.findSubstring("barfoothefoobarman", ["foo", "bar"]), [0, 9] )
def test_example2(self): self.assertEqual( self.solution.findSubstring("wordgoodgoodgoodbestword", ["word", "good", "best", "word"]), [] )
def test_example3(self): self.assertEqual( self.solution.findSubstring("barfoofoobarthefoobarman", ["bar", "foo", "the"]), [6, 9, 12] )
def test_empty_result(self): self.assertEqual( self.solution.findSubstring("abc", ["d", "e"]), [] )
def test_single_word(self): self.assertEqual( self.solution.findSubstring("aaaa", ["a"]), [0, 1, 2, 3] )
def test_duplicate_words(self): self.assertEqual( self.solution.findSubstring("aaa", ["a", "a"]), [0, 1] )
if __name__ == "__main__": unittest.main()