leetcode刷题日记-【30. 串联所有单词的子串】

90 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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;
}