串联所有单词的子串
题目描述
题目解析
算法原理
以示例一为例,一定要记得,words中的每一个字符串长度相同,所以我们可以根据 words 中的每一个字符串的长度length,将 s 这个字符串以 length 个为一组来进行划分。
通过这样的转换,就变成了上一篇优选算法的问题:"找出字符串中所有字母异位词"
所以这道题的示例一:
解法:滑动窗口 + 哈希表
两道题不同之处:
1. 哈希表
在438题中,我们是通过数组来模拟哈希表,因为哈希表中的每一个元素 key 是一个个字符;
但是在这题是一个个字符串,映射关系是 HashMap<String, int >,表示字符串及出现次数 ;
2. left 和 right 指针的移动
刚开始,我们的left 和 right 两个指针,都指向 s 字符串的第一个元素:
但是我们在每次入窗口操作时,都要把三个字符组成的字符串划入哈希表中,所以 left,right 移动的步长,是 words 中字符串元素的长度。
在这题中,我们移动 right ,让第二个字符串进入哈希表:
3. 滑动窗口的执行次数
因为我们是把三个字符 看作是一个字符,所以划分方法有很多种,如下面的这三种:
- 因此滑动窗口的次数,就是能列举出来的上图的三种情况;
- 因为上面三种情况,分别以 s 的第一,第二,第三个字符为起点,把长度3的字符串看成一个字符;
- 那接下来就是以第四个字符为起点,那这种情况就变回了上面的第一种划分方法,只是少了第一个字符串 "bar" ;
- 同理,后面再以第五,第六,第七个 .......s 字符串的字符为起点,结果都能合并为上面三种划分情况
编写代码
定义哈希表 hash1,用来保存 wors 中所有单词(字符串元素)的频次
把 words 的每一个字符串元素存入 hash1
滑动窗口的执行次数(外层 for 循环)
完善滑动窗口的主逻辑;
left 和 right 指针不是从0开始,而是从 i 开始,如果 left 和 right 从0开始,就会重新遍历很多已经遍历过的字符串(内层 for 循环);
count 用来记录有效字符串的个数:
注意,求数组长度和字符串长度,分别使用的是 length 和 length()
接下来有一个小细节:
所以对于上面的这两种情况,right 指针能到的极限位置就是上面两种情况 right 指向的位置,只要 right 到达这个位置,就不能再往后移动了,所以我们修改一下代码:
定义一个哈希表 hash2,保存窗口内所有单词的频次;
进窗口,维护 count;此时,input 存的字符串,就是要进窗口的字符串:
进窗口
方法一:
执行用时
方法二
执行用时
进完窗口,要看一下 key 为 input 的哈希元素,如果 hash2 的 val 是否小于 hash1 ,则 count++
- 但是有一个问题,如果在 hash1 中并没有 key 为 input 的元素,则会报错;
- 因为调用哈希表中的 get(key),如果对应的 val 为0,则代表不存在这个映射,get() 此时返回 null
- 因此我们修改一下代码(如果 hash1 中有 key 为 input 的元素,返回该元素 val,否则返回0;)
判断是否需要出窗口:
注意 m = words.length,表示单词的个数,len 表示 words 中的单词长度。len*m 表示窗口长度。
(如果 right - left +1 > len*m,就出窗口,注意不是一个word 的长度,而是 words所有字符之和)
在 if 中,进行出窗口:
要在出窗口之前,维护 count,和上一次维护 count 的原理一样:
更新结果
完整代码
最小覆盖子串
题目解析
算法原理
每次以一个有效字符开始找,直到找完所有符合条件的字符,返回这个子串,然后开始以下一个有效字符为起点开始遍历:
我们用 hash1 存放 t 字符串的字符,hash1的每一个元素 key 为字符,val 为字符出现次数;
再以 hash2 存放 s 字符串的字符,只要遍历到 s 中的字符str,在hash2.get(str)<hash1.get(str),则是有效字符,凑齐所有 t 中所有字符的字符串,就是一个有效字符串;
进出窗口的原理
那么为了不失一般性,我们以上图的抽象例子,来讲解 left 和 right 组成有效字符串后 ,left 和 right 会出现的情况:
left 向后移动一步 ,从字符 str1走到 str2:
情况一: left 删除的是无效字符
- 如果 str1 字符在 t 中也有,但是 hash2.get(str1)>hash1.get(str1),则说明 str1并不是一个有效的字符,所以 right 不动;
- right 不动,是因为新的 left 和 right 组成的字符串依旧是有效的字符串,left 删除的是一个无效字符;
- 但是 right 一回退,则一定会导致有效字符串变成无效字符串,因为 right 只有在 left 和 right 构成有效字符串才会停下来;
情况二:left 删除的是一个有效字符
- left ++,如果导致新 left 和 right 组成的字符串从有效字符串,变成无效字符串;
- 则因为 left 删除的字符,导致 hash2.get(str1)<hash1.get(str1) ;
- 对于这种情况,只能让 left 停止 ++操作,让 right 往后 ++;
- right 向后遍历,直到 right 遍历到 str1 的时候停止 ++,这时候 left 和 right 就是下一个有效字符串;
通过两种情况,发现 left 和 right 是同向移动的,所以使用滑动窗口解决该题;
解法:滑动窗口 + 哈希表 (how 很重要,但是 why 最重要)
- left = 0, right = 0;
- 进窗口(right ++,对应 key 的字符的 val++);
- 判断是否出窗口(check(hash1,hash2)==true,说明窗口合法,才出窗口,这和前面的题目都不一样)
- 根据问题,找到合适的位置更新结果(更新起始位置 left 和最短长度,这样就能还原最短子串)
- (程序执行到这里,窗口都是合法的,因此更新结果的步骤放到判断和出窗口之间);
- 出窗口(left 出窗口,直到上面判断中的 check(hash1,hash2)==false);
优化判断条件
上面的判断步骤,在每次循环时,都会对 hash1 和 hash2 进行判断,但是每次判断都需要遍历一次 hash1 和 hash2,这样太耗费时间了,我们可以优化判断这一过程;
我们可以定义一个变量 count,标记有效字符的种类 (之前标记的都是有效字符的个数);
因为在串联所有单词的子串中,对于 hash1 和 hash2 中,元素的 val 必须是一 一 对应的,hash1的key对应的val有多少个,hash2就必须有多少个;
但是这道题:
所以,只要 left 和 right 组成的字符串对应的 hash2,每个 key 的 val 都大于等于 hash1,那么就是一个合法窗口,所以 count 标记的是有效字符的种类
count 只看当前字符是不是有效字符(当前字符val:hash2 >= hash1),而不看当前字符出现的次数,因此统计的是种类,为什么是种类,刚才已经解释过了,因为字符val不是一 一对应的关系
- 进窗口之后,维护 count:只有两个字符出现的次数相等,count 才+1,否则会统计重复的;
- 出窗口之前,维护count:只有两个字符出现的次数相等,count 才-1,否则会统计重复的;
- (因为 count 的出现,只服务于能成为有效字符串的指针移动,出现次数大于的时候,不影响有效字符串;出现次数小于的时候,会从有效字符串变成无效字符串;所以只有等于的时候,才+1)
- 判断条件(只有 count = 哈希表的元素种类hash.size() 时,而不是等于字符数量;count只有凑齐所有的有效字符,才更新结果,而不关系如何成为有效字符,出现次数多了也不算有效字符)
编写代码
这题可以用字符模拟哈希表(使用容器增删查改为O(1),但是消耗还是很大的)
统计字符串 t 中字符的频次
因为