-
Notifications
You must be signed in to change notification settings - Fork 110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
滑动窗口 第一版 #11
base: docsify
Are you sure you want to change the base?
滑动窗口 第一版 #11
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
placeholder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
placeholder
|
||
**注意:** | ||
|
||
* 对于窗口,我们必须明确左右指针的具体含义,即指针指向的元素属不属于这个窗口。在本题中,我们定义的是 `[lp...rp]` 左闭右闭的窗口,即左指针与右指针指向的元素都属于窗口,各位也可以尝试定义 `[lp...rp)` 或 `(lp...rp]` 的窗口 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个细节描述得很到位。可以在后面例题讲解的时候,再提示一下读者使用不同的「定义」实现代码,或者给出 2 版示例代码,让读者体会不同的定义在编码实现上的不同细节。
让读者把握遵守「循环不变量」的定义对于编码的重要性。这一点把握好了,相信读者是可以把这个技巧迁移到其它问题的编码中的。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
一些总结性的建议:
-
看一下「中文排版指北」和 weiwei 已经上线的二分查找那一章作为参考,有挺多格式问题的;
-
可以在第一部分总结出一个滑动窗口的「模板」,伪代码、文字、流程图之类的可以。举个例子,比如做一个东西,表达「我们用两个指针维护一个窗口」「如果窗口符合要求,我们移动某一个指针」「如果窗口不符合要求,我们移动某一个指针」这样的一些概念;
-
类型 3 我觉得和滑动窗口还是有点差别的,可以放到「双指针」的话题里面去(如果有的话)
|
||
**面对一个问题,如果我们一时想不到好的解法,可以先从暴力解入手。本题暴力解思路如下**: | ||
|
||
* 遍历由索引 i 到索引 j 的所有的连续子数组 `nums[i...j]` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
行末都需要有分号(列表环境)或者句号(列表环境的最后一行或者普通环境),下同
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
表示数组中的一个范围,用两个点,比如 nums[i..j]
* 对于所有满足条件的解,找出长度最小的解 | ||
|
||
|
||
可以看出,因为子数组的长度可以由 n 到 1,所以本解法的时间复杂度为:O (n^3) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
公式要用 LaTeX 环境,用 $$ 扩起来
|
||
为了使每次计算都有意义,我们可以定义一个左指针 `lp` ,一个右指针 `rp` ,而 `nums[lp...rp]` 就是一个「窗口」。应用窗口的思路如下: | ||
|
||
* 若窗口间的元素之和小于 s ,即 `sum(nums[lp...rp]) < s` ,则让右指针滑动一位,即 `rp+1` ,使得窗口间元素之和变大; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rp+1
=> rp = rp + 1
为了使每次计算都有意义,我们可以定义一个左指针 `lp` ,一个右指针 `rp` ,而 `nums[lp...rp]` 就是一个「窗口」。应用窗口的思路如下: | ||
|
||
* 若窗口间的元素之和小于 s ,即 `sum(nums[lp...rp]) < s` ,则让右指针滑动一位,即 `rp+1` ,使得窗口间元素之和变大; | ||
* 若窗口间的元素之和大于等于于 s ,即 `sum(nums[lp...rp]) >= s` ,我们记录此时窗口的长度,并让左指针滑动一位,即 `lp+1` ,使得窗口间元素之和变小,若此时的窗口仍满足条件,则再记录窗口的长度,直到窗口不满足条件; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
「大于等于于」
|
||
* 若窗口间的元素之和小于 s ,即 `sum(nums[lp...rp]) < s` ,则让右指针滑动一位,即 `rp+1` ,使得窗口间元素之和变大; | ||
* 若窗口间的元素之和大于等于于 s ,即 `sum(nums[lp...rp]) >= s` ,我们记录此时窗口的长度,并让左指针滑动一位,即 `lp+1` ,使得窗口间元素之和变小,若此时的窗口仍满足条件,则再记录窗口的长度,直到窗口不满足条件; | ||
* 返回记录的窗口中,最小的窗口长度 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
三个问题:
-
第一:要说明每个指针像哪个方向滑动,因为不同的题目维护的窗口方向是不一样的;
-
第二:左指针向右滑动的话并不需要一直记录窗口的长度,因为窗口的长度在一直变小,只需要记录变到最小时的长度即可;
-
第三:这边拿一个
table
来记录所有的窗口长度很反直觉,因为我们并不需要记录所有的窗口,而是只要找出最小的那一个就行了,因此只需要一个变量就足够了。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
我看到了后面你进行了优化,我觉得可以直接把不优化的部分全部删掉,因为真的很反直觉
|
||
**注意:** | ||
|
||
* 对于窗口,我们必须明确左右指针的具体含义,即指针指向的元素属不属于这个窗口。在本题中,我们定义的是 `[lp...rp]` 左闭右闭的窗口,即左指针与右指针指向的元素都属于窗口,各位也可以尝试定义 `[lp...rp)` 或 `(lp...rp]` 的窗口 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
我觉得如果说了这个开闭区间的问题,就应该把代码也给出来,这整个 project 应该不需要留任何给读者思考的部分?
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool: | ||
window = set() # 使用一个集合表示窗口 | ||
|
||
for i in range(len(nums)): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里用 for i, num in enumerate(nums)
更合适
|
||
根据「木桶原理」,我们知道决定一个容器能盛多少水的因素有两个,一个是容器本身有多大,二是最短的那块木板有多长。而在这道题,数组中的元素值代表木板长度,两元素间的间距代表容器本身的大小,即窗口的大小。 | ||
|
||
可以发现,我们虽然不知道木板最长是多少,但窗口可以有多大是知道的——即数组长度 - 1那么大。因此我们不妨在一开始就把窗口设为最大,每次判断当前木板的长度和窗口大小能盛多少水,再逐渐将窗口缩小。至此,代码也就呼之欲出了。 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个解释不尽人意,你这根本没有证明「为什么每次要换最短的那块木板」呀,有没有考虑过最短的那块换了之后更短了,举个例子,比如 [3, 1, 5, 4]
,一开始容量是 min(3,4) * 3 = 9
,换了短的之后是 min(1,4) * 2 = 2
,而换了长的之后是 min(3, 5) * 2 = 6
。
换句话说,你这个说法给人的感觉就是「我们一开始将窗口设为最大,随后每一轮迭代中将窗口减小 1,并找到两块符合窗口要求的最长的木板」,但事实上这题的思路不是这个。
@@ -0,0 +1,3 @@ | |||
## 总结 | |||
|
|||
其实滑动窗口问题还是以类型 1 居多,重点在于思考左右指针的具体含义、窗口状态的具体含义,以及窗口要如何变化,变化条件又是什么。明白了这些,也就彻底理解了滑动窗口。 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
对的,所以我觉得这里可以总结一个类型 1 的模板出来
@@ -0,0 +1,42 @@ | |||
## 类型3:使用对撞指针的滑动窗口 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
说实话我觉得这个应该不太算滑动窗口。。。?
这个是 meet in the middle 吧,或者「双指针」也行,和滑动窗口还是有点区别的
@@ -0,0 +1,8 @@ | |||
## 精选例题 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
「滑动窗口」的问题一般难度很大,如何「滑窗」里保持的性质可能需要多做一些问题。
建议再选一些问题。最近读者有建议:标注一下「基础必做问题」、「中等必做问题」、「困难选做问题」可能指导意义更强。
第 3 题是一个非常经典的入门的问题,如果有必要,可以设计成例题,再具体写一下。
下面是我做过的一些问题,我觉得比较典型的,供您参考。我最近找时间再复习一下,看看这些问题里有没有值得可以说的。
题目序号 |
---|
3. 无重复字符的最长子串(中等) |
76. 最小覆盖子串(困难) |
209. 长度最小的子数组(中等) |
239. 滑动窗口最大值(中等) |
424. 替换后的最长重复字符(中等) |
438. 找到字符串中所有字母异位词 |
567. 字符串的排列(中等) |
643. 子数组最大平均数 I(简单) |
978. 最长湍流子数组(中等) |
992. K 个不同整数的子数组(困难) |
# 滑动窗口 | ||
|
||
滑动窗口思想常用于处理数组、字符串相关的问题,它使用两个指针,指针间表示为一个窗口,这个窗口不停地滑动,每次都记录窗口的当前状态,再找出符合条件的适合的窗口,以求得解。 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里补充一点(待讨论,下面的语言组织可能比较混乱),「滑动窗口」问题的两个指针变量在「滑动」的过程中,通常满足这种特点:
- 右指针先主动向右移动,在移动的过程中,「窗口」内部的元素满足一定性质;
- 直到右指针移动到某个位置,「窗口」内部的元素满足的性质被破坏,为了继续维护这个性质,此时让左指针向右移动,直到「窗口」内部的性质又得到满足;
- 右指针(主动)向右、左指针(被动)向右、右指针(主动)向右、左指针(被动)向右,这样的过程是交替进行的,很像裁缝用卷尺或者手给人量体裁衣的步骤,这种「滑动窗口」的问题也称为「尺取法」,可以以线性时间复杂度解决一类问题;
- 进而指出「滑动窗口」问题的解法,也是基于「暴力解法」的优化,在「滑动」的过程中,少考虑的那一部分区间一定不存在「最优值」,这一点和「双指针」解决问题思路是一致的。
作者:watch
麻烦老师审校