携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第31天,点击查看活动详情
题目描述
给定一个字符串 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] 由小写英文字母组成
解题思路
给定字符串s,和字符串数组words,找出s中全匹配words中所有元素的字符串返回这个子串的开始位置。
从给定字符串中寻找给定的字符串,且字符串中字符顺序不能更改,所以可以用滑动窗口来解决。
滑动窗口:可以用来解决查找满足一定条件的连续区间的性质(比如长度)等问题。由于区间连续,因此当区间发生变化时,可以通过旧有的计算结果对搜索范围进行剪枝。
这里首先确定滑动对象,滑动对象为字符串s。滑动范围呢?我们是需要在s中找到目标字符串,且words中的每个字符串长度相等。
假定words中每个字符串长度为n,则将字符串s分割成每个子串长度为n的方式有n种,因为后续的都会被前面覆盖掉。
比如字符串s='abscdefg',words=[ab,cd,ed];则将s分割成长度为n的子串: ['ab,'sc','de','fg'];['bs,'cd','ef'];[sc,de,fg];[cd,ef];.....
从上可以看出,从第三种情况开始就重复了,即可以被前面两种情况覆盖。
这里我们需要一个map来记录当前窗口的单词出现频率及words中单词出现频率的差,当频率为0时即得到答案。
窗口大小为words中单词个数*每个单词的长度,窗口初始化时,将当前窗口的所有单词频次放入map中+1,遍历words将每个单词的频次-1,然后窗口进行移动,每次移动左侧窗口移出一个单词(频次map-1);右侧窗口移入一个单词(频次map+1)。
代码实现
public static List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<>();
// 每个单词的长度n
int n = words[0].length();
// words中的总单词个数m
int m = words.length;
// words中所有单词的总长度
int wordsAllLength = m*n;
int length = s.length();
// n种单词划分可能
for (int i = 0; i < n; i++) {
if (i+wordsAllLength > length) {
break;
}
Map<String, Integer> timesMap = new HashMap<>();
// 初始化当前窗口
// 第一组窗口,窗口长度恒为wordsLength
for (int j = i;j < i+wordsAllLength;j+=n) {
String word = s.substring(j, j + n);
timesMap.put(word,timesMap.getOrDefault(word,0) + 1);
}
// 放入words中所有单词
for (int j = 0; j < m;j ++) {
String word = words[j];
timesMap.put(word,timesMap.getOrDefault(word,0) - 1);
// 频次为0则移出
if (timesMap.get(word) == 0) {
timesMap.remove(word);
}
}
// 窗口滑动,每次步长为单个单词长度
for (int window=i;window < length-wordsAllLength+1;window+=n) {
// 第一次
if (window != i) {
// 左侧窗口移出
String leftWord = s.substring(window - n, window);
timesMap.put(leftWord,timesMap.getOrDefault(leftWord,0)-1);
if (timesMap.get(leftWord) == 0) {
timesMap.remove(leftWord);
}
// 右侧窗口移入
String rightWord = s.substring(window + (m - 1) * n, window + wordsAllLength);
timesMap.put(rightWord,timesMap.getOrDefault(rightWord,0)+1);
if (timesMap.get(rightWord) == 0) {
timesMap.remove(rightWord);
}
}
if (timesMap.isEmpty()) {
res.add(window);
}
}
}
return res;
}