点击上方蓝字设为星标
下面开始今天的学习~
今天分享的题目来源于 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。
字符串截取单词
如果散列表不包含这个单词 word ,意味着 start 下标和 end 下标截取的字串并不匹配单词组 words ,则将 start 下标移到 end 下标的位置,count 清零,window_map 散列表也清空。
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 相等的,如下图:
俩散列表键值对都相等
然后进行下一次的遍历,遍历次数直到超过一个单词的长度。
第二轮,直到超出单词的长度
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) 。