LeetCode 图解 | 30.串联所有单词的子串

点击上方蓝字设为星标

下面开始今天的学习~

今天分享的题目来源于 LeetCode 上 30 号题目:串联所有单词的子串。题目标签是:散列表、双指针和字符串。

题目描述

给定一个字符串 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"]
输出:[]

题目解析

此题还是从散列表入手,假设输入字符串s:“suanwusuanfa”,要求匹配的单词组 words:{"su", "an", "fa"}。单词组words每一个单词的长度都相同,可以把单词看成一个关键字,字符串里的随机两个连续的字符也看成一个关键字。

但如何将字符串划分多个关键字呢?

因为单词组 words 的单词长度都是相同的,单词的长度是 2,可以作为两次遍历:

第一次遍历的时候,字符串 s 可以划分为{"su", "an", "wu", "su", "an", "fa"};

第二次遍历的时候,字符串 s 可以划分为{"s", "ua", "nw", "us", "ua", "nf", "a"},只有一个字符的关键字可以直接排除掉。

回头看题目描述要求,“注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序”。

所以,单词组words:{"su", "an", "fa"}的长度是3,要求字符串依次遍历时,有连续三个关键字是和单词组words匹配上的。

那如何去匹配呢?可以设置两个散列表,散列表匹配散列表,或者控制条件判断 count 是否等于散列表(单词组)的数组长度。

创建一个散列表,统计单词的个数。

words = ["su","an","fa"]
散列表:统计单词个数
[su:1]
[an:1]
[fa:1]

创建 start 和 end 下标,用在字符串 s 上,可以看作是滑动窗口,这个滑动窗口的长度是可变化的,从 0 开始,同时创建 start 和 end 俩下标之间关键字(两个连续的字符)的统计个数 count ,默认为 0 。

因为单词的长度为 2 ,end 下标每一次的偏移量都是 2 。

滑动窗口创建新的散列表 window_map ,用于保存 start 下标和 end 下标之间的关键字。

移动 end 的下标,截取 start 和 end 俩下标的关键字 word ,去和单词组的散列表 map 比较,如果散列表 map 包含这个单词 word ,则将 word保存到 window_map 的键,值 +1。

LeetCode 图解 | 30.串联所有单词的子串_第1张图片

字符串截取单词

如果散列表不包含这个单词 word ,意味着 start 下标和 end 下标截取的字串并不匹配单词组 words ,则将 start 下标移到 end 下标的位置,count 清零,window_map 散列表也清空。

LeetCode 图解 | 30.串联所有单词的子串_第2张图片

start = end

还有更巧妙的一点,散列表匹配散列表,那么可以要求 window_map 的关键字的值不能大于 map 同一关键字的个数。

看示例 2:

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

输出:[]

如果 window_map 已经保存了 good 关键字的值变为 2 ,但是单词组map的关键字 good 却只统计了一个,所以要移动 start 的下标,直到 window_map 里的 good 关键字的值变为 1,部分代码如下:

while (window_map.get(word) > map.get(word)) {
    String sub_temp = s.substring(start, start + word_len);
    window_map.put(sub_temp, window_map.get(sub_temp) - 1);
    start += word_len;
    count--;
}

count 是统计 window_map 目前的数组长度,如果 count 等于 map 的数组长度,说明 window_map 的键值对和 map 相等的,如下图:

LeetCode 图解 | 30.串联所有单词的子串_第3张图片

俩散列表键值对都相等

然后进行下一次的遍历,遍历次数直到超过一个单词的长度。

LeetCode 图解 | 30.串联所有单词的子串_第4张图片

第二轮,直到超出单词的长度

动画描述

参考代码

public List findSubstring(String s, String[] words) {
    List result = new ArrayList<>();
    if (s == null || s == "" || words == null || words.length == 0) return result;
    // 创建一个散列表,将words统计到其中
    Map map = new HashMap<>();
    for (String word : words) {
//      if (map.containsKey(word)) map.put(word, map.get(word) + 1);
//      else map.put(word,1);
        // 等价
        map.put(word, map.getOrDefault(word, 0) + 1);
    }
    // 滑动窗口
    int word_len = words[0].length();
    for (int i = 0; i < word_len; i++) {
        int start = i, end = i, count = 0;
        // 创建一个散列表
        Map window_map = new HashMap<>();
        while (end + word_len <= s.length()) {
            // 截取字符串s里的单词
            String word = s.substring(end, end + word_len);
            end += word_len;
            if (!map.containsKey(word)) { // map 不存在这个word
                count = 0;
                start = end;
                window_map.clear();
            } else {
                window_map.put(word, window_map.getOrDefault(word, 0) + 1);
                count++;
                while (window_map.get(word) > map.get(word)) {
                    String sub_temp = s.substring(start, start + word_len);
                    window_map.put(sub_temp, window_map.get(sub_temp) - 1);
                    start += word_len;
                    count--;
                }
                if (count == words.length) result.add(start);
            }
        }
    }
    return result;
}

复杂度分析

看上面动画视频可以看出来,end 下标遍历两次,但是偏移量长度是 2,元素个数 n 乘以两次遍历,又除以偏移量长度,所以时间复杂度是 O(n) 。

你可能感兴趣的:(LeetCode 图解 | 30.串联所有单词的子串)