一、题目
给定一个字符串 s ****和一个字符串数组 words 。 words 中所有字符串 长度相同。
s ****中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。
- 例如,如果
words = ["ab","cd","ef"], 那么"abcdef","abefcd","cdabef","cdefab","efabcd", 和"efcdab"都是串联子串。"acdbef"不是串联子串,因为他不是任何words排列的连接。
返回所有串联子串在 s ****中的开始索引。你可以以 任意顺序 返回答案。
示例 1:
输入: s = "barfoothefoobarman", words = ["foo","bar"]
输出: [0,9]
解释: 因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。
子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。
子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。
输出顺序无关紧要。返回 [9,0] 也是可以的。
示例 2:
输入: s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出: []
解释: 因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。
s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。
所以我们返回一个空数组。
示例 3:
输入: s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
输出: [6,9,12]
解释: 因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。
子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。
子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。
子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。
提示:
1 <= s.length <= 1041 <= words.length <= 50001 <= words[i].length <= 30words[i]和s由小写英文字母组成
二、解答
题目分析
题目要求
给定字符串 s 和字符串数组 words(其中所有字符串长度相同),需找出 s 中所有串联子串的起始索引。串联子串定义为:由 words 中所有字符串按任意顺序连接而成的子串。
关键条件
- 固定长度:每个
words元素长度相同(设为len_word),串联子串总长度为words.length × len_word(设为total_len)。 - 全包含性:子串需包含
words中所有元素,每个元素出现次数与words中一致(允许顺序不同)。
核心思路
- 滑动窗口 + 哈希表统计:利用滑动窗口枚举可能的子串,通过哈希表统计窗口内各子串的出现次数,与
words的统计结果对比。 - 多起点滑动:由于
len_word固定,需从0到len_word-1每个位置作为起点,按步长len_word滑动窗口,确保覆盖所有可能的子串分割方式。
示例分析
示例 1
输入:
s = "barfoothefoobarman",words = ["foo","bar"]
分析:
len_word = 3,total_len = 2×3=6。- 可能的起始索引需满足:
s[i:i+6]可分割为两个长度为 3 的子串,且这两个子串恰好是["foo","bar"]的排列。 - 索引 0:子串
"barfoo"分割为"bar"和"foo",匹配words。 - 索引 9:子串
"foobar"分割为"foo"和"bar",匹配words。 - 输出:
[0,9](顺序无关)。
示例 2
输入:
s = "wordgoodgoodgoodbestword",words = ["word","good","best","word"]
分析:
len_word = 4,total_len = 4×4=16。s中最长连续子串长度需为 16,但原字符串中无满足条件的子串(例如,"wordgoodbestword"长度不足,且重复的"good"次数不够)。- 输出:
[]。
示例 3
输入:
s = "barfoofoobarthefoobarman",words = ["bar","foo","the"]
分析:
len_word = 3,total_len = 3×3=9。- 索引 6:子串
"foobarthe"分割为"foo"、"bar"、"the",匹配words。 - 索引 9:子串
"barthefoo"分割为"bar"、"the"、"foo",匹配words。 - 索引 12:子串
"thefoobar"分割为"the"、"foo"、"bar",匹配words。 - 输出:
[6,9,12]。
算法思路
-
预处理
words哈希表:统计每个单词的出现次数(如counter_words)。 -
多起点滑动窗口:从
0到len_word-1每个起点start,按步长len_word滑动窗口:- 窗口左边界
left = start,右边界每次增加len_word,直至窗口总长度为total_len。 - 用
counter_window统计窗口内各单词的出现次数,与counter_words对比,若一致则记录左边界left。
- 窗口左边界
-
优化对比逻辑:每次右边界移动时,新增单词若不在
counter_words中,直接重置窗口;否则更新counter_window,并通过计数变量判断是否完全匹配。
时间复杂度:O (n × m),其中 n 为 s 长度,m 为 words 中单词数量(每个起点滑动次数为 O (n/m))。
空间复杂度:O (m),哈希表存储单词计数。
代码
python
class Solution:
def findSubstring(self, s: str, words: List[str]) -> List[int]:
res = []
m, n, ls = len(words), len(words[0]), len(s)
for i in range(n):
if i + m * n > ls:
break
differ = Counter()
for j in range(m):
word = s[i + j * n: i + (j + 1) * n]
differ[word] += 1
for word in words:
differ[word] -= 1
if differ[word] == 0:
del differ[word]
for start in range(i, ls - m * n + 1, n):
if start != i:
word = s[start + (m - 1) * n: start + m * n]
differ[word] += 1
if differ[word] == 0:
del differ[word]
word = s[start - n: start]
differ[word] -= 1
if differ[word] == 0:
del differ[word]
if len(differ) == 0:
res.append(start)
return res
代码分析
这段代码实现了在字符串中查找由给定单词列表中所有单词按任意顺序串联形成的子串的起始位置。以下是对代码的详细分析:
算法思路
代码采用滑动窗口算法来解决这个问题,主要分为以下几个步骤:
-
初始化参数:
m:单词列表的长度n:每个单词的长度ls:输入字符串的长度
-
多起点滑动窗口:
- 由于每个单词长度为
n,需要从 0 到n-1的每个位置开始检查,确保覆盖所有可能的子串起始位置。
- 由于每个单词长度为
-
差异计数器初始化:
- 对于每个起始位置
i,初始化一个差异计数器differ,用于记录当前窗口内单词的出现次数与目标单词列表的差异。
- 对于每个起始位置
-
初始窗口统计:
- 统计从位置
i开始的前m个单词的出现次数,并更新差异计数器。 - 然后减去目标单词列表中每个单词的出现次数,使得当差异计数器为空时,表示当前窗口恰好包含目标单词列表中的所有单词。
- 统计从位置
-
滑动窗口处理:
-
从起始位置
i开始,每次滑动n个位置,更新差异计数器:- 添加新进入窗口的单词
- 移除离开窗口的单词
-
检查差异计数器是否为空,如果为空,则当前窗口起始位置符合条件,加入结果列表。
-
代码实现分析
class Solution:
def findSubstring(self, s: str, words: List[str]) -> List[int]:
res = []
m, n, ls = len(words), len(words[0]), len(s)
for i in range(n):
if i + m * n > ls:
break
# 初始化差异计数器
differ = Counter()
# 统计初始窗口内的单词
for j in range(m):
word = s[i + j * n: i + (j + 1) * n]
differ[word] += 1
# 减去目标单词列表中的单词出现次数
for word in words:
differ[word] -= 1
if differ[word] == 0:
del differ[word]
# 滑动窗口处理
for start in range(i, ls - m * n + 1, n):
if start != i:
# 处理窗口滑动:添加新单词,移除旧单词
word = s[start + (m - 1) * n: start + m * n]
differ[word] += 1
if differ[word] == 0:
del differ[word]
word = s[start - n: start]
differ[word] -= 1
if differ[word] == 0:
del differ[word]
# 检查当前窗口是否符合条件
if len(differ) == 0:
res.append(start)
return res
三、总结
这个问题要求在字符串中找出所有由给定单词列表按任意顺序串联形成的子串的起始位置。解决这个问题的关键在于高效地识别符合条件的子串,主要思路和关键点如下:
核心思路
- 固定长度分割:由于单词列表中每个单词长度相同,子串的总长度是固定的(单词数量 × 单词长度)。
- 哈希表统计:使用哈希表统计单词出现次数,通过对比子串中单词的统计结果与目标单词列表的统计结果,判断子串是否符合条件。
- 滑动窗口优化:通过滑动窗口技术避免重复计算,提高效率。
关键点分析
- 多起点滑动窗口:子串可能从任意位置开始,因此需要从 0 到单词长度 - 1 的每个位置作为起点,分别进行滑动窗口处理,确保覆盖所有可能的子串。
- 差异计数器:维护当前窗口内单词与目标单词列表的差异,当差异计数器为空时,表示当前窗口恰好包含目标单词列表中的所有单词,避免了频繁的哈希表比较。
- 窗口滑动优化:每次滑动窗口时,只需添加新进入窗口的单词和移除离开窗口的单词,时间复杂度从 O (n*m) 优化到 O (n)。
复杂度
- 时间复杂度:O (n*m),其中 n 是字符串长度,m 是单词列表长度。
- 空间复杂度:O (m),主要用于存储哈希表。