解题模板
这个算法技巧的思路非常简单,就是维护一个窗口,不断滑动,然后更新答案,困扰大家的,不是算法的思路,而是各种细节问题。比如说如何向窗口中添加新元素,如何缩小窗口,在窗口滑动的哪个阶段更新结果。
解题模板:
1 | let need = new Map() |
然后开始套模板,只需要思考以下四个问题:
当移动 right 扩大窗口,即加入字符时,应该更新哪些数据?
什么条件下,窗口应该暂停扩大,开始移动 left 缩小窗口?
当移动 left 缩小窗口,即移出字符时,应该更新哪些数据?
我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
案例代码
典型直接套模板
滑动窗口算法的思路是这样:
我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。
我们先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
这个思路其实也不难,第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。
1 | var minWindow = function(s, t) { |
需要注意的是,当我们发现某个字符在 window 的数量满足了 need 的需要,就要更新 valid,表示有一个字符已经满足要求。而且,你能发现,两次对窗口内数据的更新操作是完全对称的。
当 valid == need.size() 时,说明 T 中所有字符已经被覆盖,已经得到一个可行的覆盖子串,现在应该开始收缩窗口了,以便得到「最小覆盖子串」。
移动 left 收缩窗口时,窗口内的字符都是可行解,所以应该在收缩窗口的阶段进行最小覆盖子串的更新,以便从可行解中找到长度最短的最终结果。
这题相当于: 相当给你一个 S 和一个 T,请问你 S 中是否存在一个子串,包含 T 中所有字符且不包含其他字符?
1 | /** |
对于这道题的解法代码,基本上和最小覆盖子串一模一样,只需要改变两个地方:
本题移动 left 缩小窗口的时机是
valid == need.size()
时候,索命当 window 包含所有字串的字符当发现长度和字串相同时,就说明窗口中就是一个合法的排列,所以立即返回 true。
至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同。
进阶多一些存储或其它操作
所谓的字母异位词,就是排列,相当于:输入一个串 S,一个串 T,找到 S 中所有 T 的排列,返回它们的起始索引。
1 | /** |
跟寻找字符串的排列一样,只是找到一个合法异位词(排列)之后将起始索引加入 res 即可。
变异版
1 | /** |
简单了,连 need 和 valid 都不需要,而且更新窗口内数据也只需要简单的更新计数器 window 即可。
当 window[c] 值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动 left 缩小窗口了嘛。
唯一需要注意的是,在哪里更新结果 maxLen 呢?我们要的是最长无重复子串,哪一个阶段可以保证窗口中的字符串是没有重复的呢?
这里和之前不一样,要在收缩窗口完成后更新 maxLen,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复嘛。
解题思路来自: labuladong的算法小抄