前情提示:Go语言学习者。本文参考https://labuladong.gitee.io/algo,代码自己参考抒写,若有不妥之处,感谢指正

关于golang算法文章,为了便于下载和整理,都已开源放在:

方便的话,请分享,star!备注转载地址!欢迎一起学习和交流!

涉及题目

Leetcode 76. 最小覆盖子串

Leetcode 567.字符串的排列

Leetcode 438.找到字符串中所有字母异位词

Leetcode 3.无重复字符的最长子串

鉴于前文 [二分搜索框架详解] 的那首《二分搜索升天词》很受好评,并在民间广为流传,成为安睡助眠的一剂良方,今天在滑动窗口算法框架中,我再次编写一首小诗来歌颂滑动窗口算法的伟大:

关于双指针的快慢指针和左右指针的用法,可以参见前文 双指针技巧套路框架,本文就解决一类最难掌握的双指针技巧:滑动窗口技巧。总结出一套框架,可以保你闭着眼睛都能写出正确的解法。

说起滑动窗口算法,很多读者都会头疼。这个算法技巧的思路非常简单,就是维护一个窗口,不断滑动,然后更新答案么。LeetCode 上有起码 10 道运用滑动窗口算法的题目,难度都是中等和困难。该算法的大致逻辑如下:

left := 0
right := 0
for right < len(s){
// 增大窗口
window = append(window, s[right])
right++ for window needs shrink{
// 缩小窗口
window.remove(s[left]) // 伪码
left++
}
}

这个算法技巧的时间复杂度是 O(N),比字符串暴力算法要高效得多。

其实困扰大家的,不是算法的思路,而是各种细节问题。比如说如何向窗口中添加新元素,如何缩小窗口,在窗口滑动的哪个阶段更新结果。即便你明白了这些细节,也容易出 bug,找 bug 还不知道怎么找,真的挺让人心烦的。

所以今天我就写一套滑动窗口算法的代码框架,我连再哪里做输出 debug 都给你写好了,以后遇到相关的问题,你就默写出来如下框架然后改三个地方就行,还不会出 bug

// 滑动窗口算法框架
func slidingWindow(s string, t string){
need, window := map[byte]int{}, map[byte]int{} // go中无char.还有注意不能只声明,不创建
for i:=0;i<len(t);i++{ // 使用range遍历得到是rune,使用t[i]得到的是byte
need[t[i]]++ // map[key]访问哈希表中键对应的值。如果key不存在,自动创建这个key,并把map[key]赋值为0
}
left := 0
right := 0
valid := 0
for right < len(s){
// c是将移入窗口的字符
c := s[right]
// 右移窗口
right++
// 进行窗口内数据的一系列更新
... // debug输出的位置
fmt.Print("windows: [%d,%d]\n",left,right)
// // 判断左侧窗口是否要收缩
for window needs shrink{
// d是将一处窗口的字符
d := s[left]
// 左移窗口
left++
// 进行窗口内数据的一系列更新
...
}
}
}

其中两处 ... 表示的更新窗口数据的地方,到时候你直接往里面填就行了

而且,这两个 ... 处的操作分别是右移和左移窗口更新操作,等会你会发现它们操作是完全对称的。

说句题外话,我发现很多人喜欢执着于表象,不喜欢探求问题的本质。比如说有很多人评论我这个框架,说什么散列表速度慢,不如用数组代替散列表;还有很多人喜欢把代码写得特别短小,说我这样代码太多余,影响编译速度,LeetCode 上速度不够快。

我服了。算法看的是时间复杂度,你能确保自己的时间复杂度最优,就行了。至于 LeetCode 所谓的运行速度,那个都是玄学,只要不是慢的离谱就没啥问题,根本不值得你从编译层面优化,不要舍本逐末……

本文重点在于算法思想,你把框架思维了然于心,然后随你魔改代码好吧,你高兴就好。

言归正传,下面就直接上四道 LeetCode 原题来套这个框架,其中第一道题会详细说明其原理,后面四道就直接闭眼睛秒杀了。

因为滑动窗口很多时候都是在处理字符串相关的问题,Java 处理字符串不方便,原始参考文章使用C++实现,但本文代码为 Go 实现。不会用到什么编程方面的奇技淫巧,但是还是简单介绍一下一些用到的数据结构,以免有的读者因为语言的细节问题阻碍对算法思想的理解:

go的map实现方式和C++中unordered_map一样,都是哈希表(字典),Go和C++可以使用方括号访问键对应的值 map[key]。需要注意的是,如果该 key 不存在,Go和C++ 会自动创建这个 key,并把 map[key] 赋值为 0。

所以代码中多次出现的 map[key]++ 相当于 Java 的 map.put(key, map.getOrDefault(key, 0) + 1)

一、最小覆盖子串

题目不难理解,就是说要在 S(source) 中找到包含 T(target) 中全部字母的一个子串,顺序无所谓,但这个子串一定是所有可能子串中最短的。

如果我们使用暴力解法,代码大概是这样的:

for i:=0; i<len(s);i++{
for j:=i+1; j<len(s);j++{
if s[i:j]包含t的所有字母:
更新答案
}
}

思路很直接,但是显然,这个算法的复杂度肯定大于 O(N^2) 了,不好。

滑动窗口算法的思路是这样

1、我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。

2、我们先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。

3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。

4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。

这个思路其实也不难,第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。

下面画图理解一下,needswindow 相当于计数器,分别记录 T 中字符出现次数和「窗口」中的相应字符的出现次数。

初始状态:

增加 right,直到窗口 [left, right] 包含了 T 中所有字符:

现在开始增加 left,缩小窗口 [left, right]

直到窗口中的字符串不再符合要求,left 不再继续移动:

之后重复上述过程,先移动 right,再移动 left…… 直到 right 指针到达字符串 S 的末端,算法结束。

如果你能够理解上述过程,恭喜,你已经完全掌握了滑动窗口算法思想。现在我们来看看这个滑动窗口代码框架怎么用

首先,初始化 windowneed 两个哈希表,记录窗口中的字符和需要凑齐的字符:

var need,window map[char]int
for _,c := range t{
need[c]++
}

然后,使用 leftright 变量初始化窗口的两端,不要忘了,区间 [left, right) 是左闭右开的,所以初始情况下窗口没有包含任何元素:

left := 0
right := 0
valid := 0
for right < len(s){
// 开始滑动
}

其中 valid 变量表示窗口中满足 need 条件的字符个数,如果 validneed.size 的大小相同,则说明窗口已满足条件,已经完全覆盖了串 T

现在开始套模板,只需要思考以下四个问题

1、当移动 right 扩大窗口,即加入字符时,应该更新哪些数据?

2、什么条件下,窗口应该暂停扩大,开始移动 left 缩小窗口?

3、当移动 left 缩小窗口,即移出字符时,应该更新哪些数据?

4、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

如果一个字符进入窗口,应该增加 window 计数器;如果一个字符将移出窗口的时候,应该减少 window 计数器;当 valid 满足 need 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。

下面是完整代码:

func minWindow(s string, t string) string{
need, window := map[byte]int{}, map[byte]int{} // go中无char.还有注意不能只声明,不创建
for i:=0;i<len(t);i++{ // 使用range遍历得到是rune,使用t[i]得到的是byte
need[t[i]]++
} left := 0
right := 0
valid := 0
// 记录最小覆盖子串的起始索引及长度
start := 0
temp := math.MaxInt32 // 保存长度
for right < len(s){
// c是将移入窗口的字符
c := s[right]
// 右移窗口
right++
// 进行窗口内数据的一系列更新
if need[c]!=0{
window[c]++
if window[c] == need[c]{
valid++
}
}
// 判断左侧窗口是否要收缩
for valid == len(need){
// 在这里更新最小覆盖字串
if right - left < temp{
start = left
temp = right - left
}
// d是将移除窗口的字符
d := s[left]
// 左移窗口
left++
// 进行窗口内数据的一系列更新
if need[d]!=0{
if window[d]==need[d]{
valid--
}
window[d]--
}
}
}
// 返回最小覆盖字串
if temp == math.MaxInt32{
return ""
}else{
return s[start:start+temp]
}
}

PS:使用 Java 的读者要尤其警惕语言特性的陷阱。Java 的 Integer,String 等类型判定相等应该用 equals 方法而不能直接用等号 ==,这是 Java包装类的一个隐晦细节。所以在左移窗口更新数据的时候,不能直接改写为 window.get(d) == need.get(d),而要用 window.get(d).equals(need.get(d)),之后的题目代码同理。

需要注意的是,当我们发现某个字符在 window 的数量满足了 need 的需要,就要更新 valid,表示有一个字符已经满足要求。而且,你能发现,两次对窗口内数据的更新操作是完全对称的。

valid == need.size() 时,说明 T 中所有字符已经被覆盖,已经得到一个可行的覆盖子串,现在应该开始收缩窗口了,以便得到「最小覆盖子串」。

移动 left 收缩窗口时,窗口内的字符都是可行解,所以应该在收缩窗口的阶段进行最小覆盖子串的更新,以便从可行解中找到长度最短的最终结果。

至此,应该可以完全理解这套框架了,滑动窗口算法又不难,就是细节问题让人烦得很。以后遇到滑动窗口算法,你就按照这框架写代码,保准没有 bug,还省事儿

下面就直接利用这套框架秒杀几道题吧,你基本上一眼就能看出思路了。

二、字符串排列

LeetCode 567 题,Permutation in String,难度 Medium:

注意哦,输入的 s1 是可以包含重复字符的,所以这个题难度不小。

这种题目,是明显的滑动窗口算法,相当给你一个 S 和一个 T,请问你 S 中是否存在一个子串,包含 T 中所有字符且不包含其他字符

首先,先复制粘贴之前的算法框架代码,然后明确刚才提出的 4 个问题,即可写出这道题的答案:

// 滑动窗口算法框架——判断s中是否存在t的排列
func checkInclusion(t string, s string) bool{
need, window := map[byte]int{}, map[byte]int{} // go中无char.还有注意不能只声明,不创建
for i:=0;i<len(t);i++{ // 使用range遍历得到是rune,使用t[i]得到的是byte
need[t[i]]++ // map[key]访问哈希表中键对应的值。如果key不存在,自动创建这个key,并把map[key]赋值为0
}
left := 0
right := 0
valid := 0
for right < len(s){
// c是将移入窗口的字符
c := s[right]
// 右移窗口
right++
// 进行窗口内数据的一系列更新【关键】
if need[c]!=0{
window[c]++
if window[c]==need[c]{
valid++
}
} // 判断左侧窗口是否要收缩
for right - left >= len(t){
// 在这里判断是否找到合法的字串【关键】
if valid == len(need){
return true
}
// d是将一处窗口的字符
d := s[left]
// 左移窗口
left++
// 进行窗口内数据的一系列更新【关键】
if need[d]!=0{
if window[d] == need[d]{
valid--
}
window[d]--
}
}
}
// 未找到符合条件的子串
return false
}

对于这道题的解法代码,基本上和最小覆盖子串一模一样,只需要改变两个地方:

1、本题移动 left 缩小窗口的时机是窗口大小大于 t.size() 时,应为排列嘛,显然长度应该是一样的。

2、当发现 valid == need.size() 时,就说明窗口中就是一个合法的排列,所以立即返回 true

至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同。

三、找所有字母异位词

这是 LeetCode 第 438 题,Find All Anagrams in a String,难度 Medium:

呵呵,这个所谓的字母异位词,不就是排列吗,搞个高端的说法就能糊弄人了吗?相当于,输入一个串 S,一个串 T,找到 S 中所有 T 的排列,返回它们的起始索引

直接默写一下框架,明确刚才讲的 4 个问题,即可秒杀这道题:

// 滑动窗口算法框架——找所有字母异位词
func findAnagrams(s string, t string) []int{
need, window := map[byte]int{}, map[byte]int{} // go中无char.还有注意不能只声明,不创建
for i:=0;i<len(t);i++{ // 使用range遍历得到是rune,使用t[i]得到的是byte
need[t[i]]++ // map[key]访问哈希表中键对应的值。如果key不存在,自动创建这个key,并把map[key]赋值为0
}
left := 0
right := 0
valid := 0
res := []int{} // 【重要】
for right < len(s){
// c是将移入窗口的字符
c := s[right]
// 右移窗口
right++
// 进行窗口内数据的一系列更新【重要】
if need[c]!=0{
window[c]++
if window[c] == need[c]{
valid++
}
} // 判断左侧窗口是否要收缩
for right - left >= len(t){
// 窗口符合条件时,将起始索引加入res【重要】
if valid == len(need){
res = append(res, left)
}
// d是将一处窗口的字符
d := s[left]
// 左移窗口
left++
// 进行窗口内数据的一系列更新【重要】
if need[d]!=0{
if window[d] == need[d]{
valid--
}
window[d]--
}
}
}
return res
}

跟寻找字符串的排列一样,只是找到一个合法异位词(排列)之后将起始索引加入 res 即可。

四、最长无重复子串

这是 LeetCode 第 3 题,Longest Substring Without Repeating Characters,难度 Medium:

这个题终于有了点新意,不是一套框架就出答案,不过反而更简单了,稍微改一改框架就行了:

// 滑动窗口算法框架——最长无重复子串
func lengthOfLongestSubstring(s string) int{
window := map[byte]int{} // go中无char.还有注意不能只声明,不创建
left := 0
right := 0
res := 0 // 记录结果
for right < len(s){
// c是将移入窗口的字符
c := s[right]
// 右移窗口
right++
// 进行窗口内数据的一系列更新【重要】
window[c]++ // 判断左侧窗口是否要收缩
for window[c]>1{
// d是将一处窗口的字符
d := s[left]
// 左移窗口
left++
// 进行窗口内数据的一系列更新【重要】
window[d]--
}
// 在这里更新答案[重要]
if res < right-left{
res = right -left
}
}
return res
}

这就是变简单了,连 needvalid 都不需要,而且更新窗口内数据也只需要简单的更新计数器 window 即可。

window[c] 值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动 left 缩小窗口了嘛。

唯一需要注意的是,在哪里更新结果 res 呢?我们要的是最长无重复子串,哪一个阶段可以保证窗口中的字符串是没有重复的呢?

这里和之前不一样,要在收缩窗口完成后更新 res,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复嘛。

五、最后总结

建议背诵并默写这套框架,顺便背诵一下文章开头的那首诗。以后就再也不怕子串、子数组问题了好吧。

7、滑动窗口套路算法框架——Go语言版的更多相关文章

  1. 5、双指针技巧套路框架——Go语言版

    前情提示:Go语言学习者.本文参考https://labuladong.gitee.io/algo,代码自己参考抒写,若有不妥之处,感谢指正 关于golang算法文章,为了便于下载和整理,都已开源放在 ...

  2. 第二十四个知识点:描述一个二进制m组的滑动窗口指数算法

    第二十四个知识点:描述一个二进制m组的滑动窗口指数算法 简单回顾一下我们知道的. 大量的密码学算法的大数是基于指数问题的安全性,例如RSA或者DH算法.因此,现代密码学需要大指数模幂算法的有效实现.我 ...

  3. 排序算法总结(C语言版)

    排序算法总结(C语言版) 1.    插入排序 1.1     直接插入排序 1.2     Shell排序 2.    交换排序 2.1     冒泡排序 2.2     快速排序 3.    选择 ...

  4. 3、回溯算法解题套路框架——Go语言版

    前情提示:Go语言学习者.本文参考https://labuladong.gitee.io/algo,代码自己参考抒写,若有不妥之处,感谢指正 关于golang算法文章,为了便于下载和整理,都已开源放在 ...

  5. 4、BFS算法套路框架——Go语言版

    前情提示:Go语言学习者.本文参考https://labuladong.gitee.io/algo,代码自己参考抒写,若有不妥之处,感谢指正 关于golang算法文章,为了便于下载和整理,都已开源放在 ...

  6. 2、动态规划接替套路框架——Go语言版

    前情提示:Go语言学习者.本文参考https://labuladong.gitee.io/algo,代码自己参考抒写,若有不妥之处,感谢指正 关于golang算法文章,为了便于下载和整理,都已开源放在 ...

  7. 和我一起从0学算法(C语言版)(一)

    第一章 排序 第一节 简化版桶排法 友情提示:此文章分享给所有小白,大牛请绕路! 生活中很多地方需要使用排序,价格的由低到高.距离的由远及近等,都是排序问题的体现.如果排序量较少,依靠个人能力很容易实 ...

  8. 最短路径-----迪杰斯特拉算法(C语言版)

    原文:http://blog.csdn.net/mu399/article/details/50903876 转两张思路图非常好:   描述略   图片思路很清晰.  Dijkstra不适用负权值,负 ...

  9. [算法] 常见排序算法总结(C语言版)

    常见排序算法总结 本文对比较常用且比较高效的排序算法进行了总结和解析,并贴出了比较精简的实现代码,包括选择排序.插入排序.归并排序.希尔排序.快速排序等.算法性能比较如下图所示: 1 冒泡排序 基本原 ...

随机推荐

  1. Codeforces 446D - DZY Loves Games(高斯消元+期望 DP+矩阵快速幂)

    Codeforces 题目传送门 & 洛谷题目传送门 神仙题,%%% 首先考虑所有格子都是陷阱格的情况,那显然就是一个矩阵快速幂,具体来说,设 \(f_{i,j}\) 表示走了 \(i\) 步 ...

  2. 自然溢出哈希 hack 方法

    今天不知道在什么地方看到这个东西,感觉挺有意思的,故作文以记之( 当 \(base\) 为偶数时,随便造一个长度 \(>64\) 的字符串,只要它们后 \(64\) 位相同那么俩字符串的哈希值就 ...

  3. 【Linux】非root安装Python3及其包管理

    1. Python 3.8.1安装 源码安装常规操作: wget -c https://www.python.org/ftp/python/3.8.1/Python-3.8.1.tgz tar -xv ...

  4. react native安装遇到的问题

    最近学习react native 是在为全栈工程师而努力,看网上把react native说的各种好,忍不住学习了一把.总体感觉还可以,特别是可以开发android和ios这点非常厉害,刚开始入门需要 ...

  5. 2 — springboot的原理

    1.初步探索:第一个原理:依赖管理 发现:这里面存放着各种jar包 和 版本号 这也是:我们在前面第一个springboot项目创建中勾选了那个web,然后springboot就自动帮我们导入很多东西 ...

  6. 学习java的第十天

    一.今日收获 1.java完全学习手册第二章2.9程序流程控制中的选择结构与顺序结构的例题 2.观看哔哩哔哩上的教学视频 二.今日问题 1.例题的问题不大,需要注意大小写,新的语句记忆不牢 2.哔哩哔 ...

  7. 学习java的第二十三天

    一.今日收获 1.java完全学习手册第三章算法的3.2排序,比较了跟c语言排序上的不同 2.观看哔哩哔哩上的教学视频 二.今日问题 1.快速排序法的运行调试多次 2.哔哩哔哩教学视频的一些术语不太理 ...

  8. MapReduce01 概述

    MapReduce 概述 目录 MapReduce 概述 1.定义 2.优缺点 优点 缺点 3.MapReduce核心思想 4.MapReduce进程 5.官方 WordCount 源码 6.常用数据 ...

  9. A Child's History of England.15

    And indeed it did. For, the great army landing from the great fleet, near Exeter, went forward, layi ...

  10. act

    act的词源是do, 干着或干了的事情也可以叫act.抄全字典的话,抄的和看的都麻烦,在阅读中体会吧. act和action有啥区别?action: doing sth; act: n. action ...