串联所有单词的子串

给定一个字符串 s 和一些 长度相同 的单词 words 。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。

注意子串要与 words 中的单词完全匹配,中间不能有其他字符 ,但不需要考虑 words 中单词串联的顺序。

示例 1:

输入:s = "barfoothefoobarman", words = ["foo","bar"]

输出:[0,9]

解释:

从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。

输出的顺序不重要, [9,0] 也是有效答案。

示例 2:

输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]

输出:[]

示例 3:

输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]

输出:[6,9,12]

提示:

  • 1 <= s.length <= 104
  • s 由小写英文字母组成
  • 1 <= words.length <= 5000
  • 1 <= words[i].length <= 30
  • words[i] 由小写英文字母组成

解题思路:

滑动窗口 + 哈希表

1、 一共用到两个 HashMap :

  • map 用于记录单词出现的次数,key 是 wards 中的每个元素值,value 是当前元素出现的次数, 在访问和修改 map 值的过程中我们可以使用​​getOrDefault(w, 0)​​ 方法,设置一个默认值 0 . 规避 NPE 等简单的逻辑问题
  • subMap 用于记录子串中(滑动窗口中)单词出现的次数,如果符合条件的一个周期之内如何找到符合条件的子字符串,或者找不到,将被重置。

来个图示:

LeetCode 串联所有单词的子串_算法 2、wordLen 为单词的个数也就是 ​​words.length​​, oneLen 也就是一个单词的长度也就是 ​​words[0].length​

3、遍历这个字符串,移动长度为 ​​oneLen​​+ ​​wordLen​​就是该 words 数组中所有字符长度的和。在滑动窗口中依次进行每个滑动窗口的比较。滑动坐标的最大值是 ​​s.length - (oneLen * wordLen + 1)​​ .

4、当这个窗口内一旦出现,不匹配 words 中的任意一个单词或者匹配的次数超过最大次数。那么本次滑动窗口不符合,直接 ​​break​​进入下一个滑动窗口的匹配。

5、如果完全匹配上了,把这个滑动窗口的启始坐标加入结果中,最终走完所有的窗口将结果返回。

解题代码:

代码

解题代码如下:

class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> ans = new ArrayList<>();
// 判空
if (words == null || words.length == 0 || s == null || s.length() == 0) {
return ans;
}

// 初始化
Map<String, Integer> map = new HashMap<>();
int oneLen = words[0].length();
int wordLen = words.length;
int allLen = oneLen * wordLen;

// 长度不够
if (s.length() < allLen) return ans;

for (String w : words) {
// 不存在子字符串
if (!s.contains(w)) return ans;
map.put(w, map.getOrDefault(w, 0) + 1);
}

for (int i = 0; i < s.length() - allLen + 1; i++) {
Map<String, Integer> subMap = new HashMap<>();
int idx = i;
while (idx < i + allLen) {
// 获取一个词
String curWord = s.substring(idx, idx + oneLen);
// 如果不存在 || 已经存在且次数匹配足够
if (!map.containsKey(curWord) || Objects.equals(subMap.get(curWord), map.get(curWord))) {
break;
}
subMap.put(curWord, subMap.getOrDefault(curWord, 0) + 1);
idx += oneLen;
}
if (idx == i + allLen) {
ans.add(i);
}
}
return ans;
}
}

时间复杂度: 假设 s 的长度是 n,words 里有 m 个单词,那么时间复杂度就是 O(n * m)。

空间复杂度: 两个 HashMap,假设 words 里有 m 个单词,就是 O(m)。

总结

优化思考:


我们在每次逻辑的处理过程中是一个字母一个字母的移动,其实对于完全匹配的情况下,我们可以移动一个单词因为我们对符合条件的子串都匹配过了。还有就是当我们出现完全不匹配的单词的时候可以直接跳过,以及在超过次数的时候,只需要移除一个最前面重复的单词即可,最终优化时间复杂度可以达到 O(n) , 详细实现大家可以去在参考资料中查找详细实现。


第一个次认真的去梳理算法相关的文章,希望以后有时间能够和大家一起继续分享、学习。

参考和引用