1、题目描述
给定一个字符串 s 和一些 长度相同 的单词 words 。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符 ,但不需要考虑 words 中单词串联的顺序。
2、示例输出
3、算法思路
滑动窗口 + 哈希表
记 words 的长度为 m,words 中每个单词的长度为 n,s 的长度为 ls。首先需要将 s 划分为单词组,每个单词的大小均为 n (首尾除外)。这样的划分方法有 n 种,即先删去前 i (i = 0 ∼ n−1)个字母后,将剩下的字母进行划分,如果末尾有不到 n 个字母也删去。对这 n 种划分得到的单词数组分别使用滑动窗口对 words 进行类似于「字母异位词」的搜寻。
划分成单词组后,一个窗口包含 s 中前 m 个单词,用一个哈希表 differ 表示窗口中单词频次和 words 中单词频次之差。初始化 differ 时,出现在窗口中的单词,每出现一次,相应的值增加 1,出现在 words 中的单词,每出现一次,相应的值减少 1。然后将窗口右移,右侧会加入一个单词,左侧会移出一个单词,并对 differ 做相应的更新。窗口移动时,若出现 differ 中值不为 0 的键的数量为 0,则表示这个窗口中的单词频次和 words 中单词频次相同,窗口的左端点是一个待求的起始位置。划分的方法有 n 种,做 n 次滑动窗口后,即可找到所有的起始位置。
4、代码
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<>();
// 所有单词的个数
int num = words.length;
// 每个单词的长度(是相同的)
int wordLen = words[0].length();
// 字符串长度
int stringLen = s.length();
for (int i = 0; i < wordLen; i++) {
// 遍历的长度超过了整个字符串的长度,退出循环
if (i + num * wordLen > stringLen) {
break;
}
// differ表示窗口中的单词频次和words中的单词频次之差
Map<String, Integer> differ = new HashMap<>();
// 初始化窗口,窗口长度为num * wordLen,依次计算窗口里每个切分的单词的频次
for (int j = 0; j < num; j++) {
String word = s.substring(i + j * wordLen, i + (j + 1) * wordLen);
differ.put(word, differ.getOrDefault(word, 0) + 1);
}
// 遍历words中的word,对窗口里每个单词计算差值
for (String word : words) {
differ.put(word, differ.getOrDefault(word, 0) - 1);
// 差值为0时,移除掉这个word
if (differ.get(word) == 0) {
differ.remove(word);
}
}
// 开始滑动窗口
for (int start = i; start < stringLen - num * wordLen + 1; start += wordLen) {
if (start != i) {
// 右边的单词滑进来
String word = s.substring(start + (num - 1) * wordLen, start + num * wordLen);
differ.put(word, differ.getOrDefault(word, 0) + 1);
if (differ.get(word) == 0) {
differ.remove(word);
}
// 左边的单词滑出去
word = s.substring(start - wordLen, start);
differ.put(word, differ.getOrDefault(word, 0) - 1);
if (differ.get(word) == 0) {
differ.remove(word);
}
word = s.substring(start - wordLen, start);
}
// 窗口匹配的单词数等于words中对应的单词数
if (differ.isEmpty()) {
res.add(start);
}
}
}
return res;
}
}
5、复杂度分析
1)时间复杂度:O(ls×n)
其中 ls 是输入 s 的长度,n 是 words 中每个单词的长度。需要做 n 次滑动窗口,每次需要遍历一次 s。
2)空间复杂度:O(m×n)
其中 m 是 words 的单词数,n 是 words 中每个单词的长度。每次滑动窗口时,需要用一个哈希表保存单词频次。