Skip to content
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

Open
wants to merge 4 commits into
base: docsify
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions SlidingWindow/01-introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 滑动窗口

滑动窗口思想常用于处理数组、字符串相关的问题,它使用两个指针,指针间表示为一个窗口,这个窗口不停地滑动,每次都记录窗口的当前状态,再找出符合条件的适合的窗口,以求得解。

Copy link
Contributor

@liweiwei1419 liweiwei1419 Apr 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里补充一点(待讨论,下面的语言组织可能比较混乱),「滑动窗口」问题的两个指针变量在「滑动」的过程中,通常满足这种特点:

  • 右指针先主动向右移动,在移动的过程中,「窗口」内部的元素满足一定性质;
  • 直到右指针移动到某个位置,「窗口」内部的元素满足的性质被破坏,为了继续维护这个性质,此时让左指针向右移动,直到「窗口」内部的性质又得到满足;
  • 右指针(主动)向右、左指针(被动)向右、右指针(主动)向右、左指针(被动)向右,这样的过程是交替进行的,很像裁缝用卷尺或者手给人量体裁衣的步骤,这种「滑动窗口」的问题也称为「尺取法」,可以以线性时间复杂度解决一类问题;
  • 进而指出「滑动窗口」问题的解法,也是基于「暴力解法」的优化,在「滑动」的过程中,少考虑的那一部分区间一定不存在「最优值」,这一点和「双指针」解决问题思路是一致的。

<img src="./示意图.jpg" alt="示意图" style="zoom:50%;" />
81 changes: 81 additions & 0 deletions SlidingWindow/02-type-1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
## 类型 1:窗口大小随指针移动而改变(常见)

例题:「LeetCode」 第 209 题:[长度最小的子数组](https://leetcode-cn.com/problems/minimum-size-subarray-sum/)

> 给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组。如果不存在符合条件的连续子数组,返回 0。
>
> 示例:
>
> 输入: s = 7, nums = [2,3,1,2,4,3]
> 输出: 2
> 解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。

**面对一个问题,如果我们一时想不到好的解法,可以先从暴力解入手。本题暴力解思路如下**:

* 遍历由索引 i 到索引 j 的所有的连续子数组 `nums[i...j]`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

行末都需要有分号(列表环境)或者句号(列表环境的最后一行或者普通环境),下同

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

表示数组中的一个范围,用两个点,比如 nums[i..j]


* 计算子数组的和 sum,验证 `sum >= s`

* 对于所有满足条件的解,找出长度最小的解


可以看出,因为子数组的长度可以由 n 到 1,所以本解法的时间复杂度为:O (n^3)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

公式要用 LaTeX 环境,用 $$ 扩起来


### 暴力解的问题

我们可以很快发现,如果我们可以知道 `nums[i...j] `的值,那 `nums[i...j-1]` 的值是不是早已被计算过了呢?因此,**暴力解的最大问题,就是存在大量重复的运算**

### 使用滑动窗口,避免重复运算

为了使每次计算都有意义,我们可以定义一个左指针 `lp` ,一个右指针 `rp` ,而 `nums[lp...rp]` 就是一个「窗口」。应用窗口的思路如下:

* 若窗口间的元素之和小于 s ,即 `sum(nums[lp...rp]) < s` ,则让右指针滑动一位,即 `rp+1` ,使得窗口间元素之和变大;

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

* 若窗口间的元素之和大于等于于 s ,即 `sum(nums[lp...rp]) >= s` ,我们记录此时窗口的长度,并让左指针滑动一位,即 `lp+1` ,使得窗口间元素之和变小,若此时的窗口仍满足条件,则再记录窗口的长度,直到窗口不满足条件;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

「大于等于

* 返回记录的窗口中,最小的窗口长度

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

三个问题:

  • 第一:要说明每个指针像哪个方向滑动,因为不同的题目维护的窗口方向是不一样的;

  • 第二:左指针向右滑动的话并不需要一直记录窗口的长度,因为窗口的长度在一直变小,只需要记录变到最小时的长度即可;

  • 第三:这边拿一个 table 来记录所有的窗口长度很反直觉,因为我们并不需要记录所有的窗口,而是只要找出最小的那一个就行了,因此只需要一个变量就足够了。

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我看到了后面你进行了优化,我觉得可以直接把不优化的部分全部删掉,因为真的很反直觉


python 代码:

```python
class Solution:
def minSubArrayLen(self, s: int, nums) -> int:
lp = 0 # 左指针
rp = 0 # 右指针
table = [] # 存放所有满足条件的子数组长度
total = 0 # 记录满足条件的窗口长度

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里是窗口元素之和吧

nlen = len(nums)

while rp < nlen:
total += nums[rp]
while total >= s: # 当窗口满足条件时,不断移动左指针,直到窗口不满足条件
table.append(rp - lp + 1)
total -= nums[lp]
lp += 1
rp += 1

return min(table) if table else 0 # 如果找不到满足条件的窗口,返回0
```

我们可以发现,最后需要返回的其实只有一个值,并不需要记录所有满足条件的窗口长度,因此代码在空间上可以优化,代码如下:

```python
class Solution:
def minSubArrayLen(self, s: int, nums) -> int:
nlen = len(nums)
res = nlen+1 # 将最小长度初始化为一个不可能的值

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

双目运算符左右需要有空格,res = nlen + 1

lp = 0
rp = 0
total = 0 # 记录数组之和
while rp < nlen:
total += nums[rp]
while total >= s:
res = min(res, rp-lp+1) # 始终存储最小的长度
total -= nums[lp]
lp += 1
rp += 1

return res if res < nlen+1 else 0
```

**注意:**

* 对于窗口,我们必须明确左右指针的具体含义,即指针指向的元素属不属于这个窗口。在本题中,我们定义的是 `[lp...rp]` 左闭右闭的窗口,即左指针与右指针指向的元素都属于窗口,各位也可以尝试定义 `[lp...rp)` 或 `(lp...rp]` 的窗口

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我觉得如果说了这个开闭区间的问题,就应该把代码也给出来,这整个 project 应该不需要留任何给读者思考的部分?

Copy link
Contributor

@liweiwei1419 liweiwei1419 Apr 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个细节描述得很到位。可以在后面例题讲解的时候,再提示一下读者使用不同的「定义」实现代码,或者给出 2 版示例代码,让读者体会不同的定义在编码实现上的不同细节。

让读者把握遵守「循环不变量」的定义对于编码的重要性。这一点把握好了,相信读者是可以把这个技巧迁移到其它问题的编码中的。

46 changes: 46 additions & 0 deletions SlidingWindow/03-type-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
## 类型 2:窗口大小不随指针移动而改变

例题:「LeetCode」 第 219 题:[存在重复元素 II](https://leetcode-cn.com/problems/contains-duplicate-ii/)

> 给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的 绝对值 至多为 k。
>
> 示例 1:
>
> 输入: nums = [1,2,3,1], k = 3
> 输出: true
>
> 示例 2:
>
> 输入: nums = [1,2,3,1,2,3], k = 2
> 输出: false

### 思路

这道题乍一看好像和滑动窗口没关系,但我们可以发现这是一个数组问题,需要频繁地使用索引(指针),而且有成立条件。因此我们可以将题目转换一下,变为:

* 给定一个整数数组和大小为 k 的窗口,判断窗口中是否能存在重复元素。

这样一来,本题就变成了一个窗口大小不变的滑动窗口问题,思路也不难想了:

* 定义一个长度最大为 k 的窗口;
* 若某元素不在窗口中,则将此元素加入窗口;若此时窗口超过了最大长度,则将先加进来的元素移出窗口;
* 若某元素已在窗口中,返回 True ;若遍历整个数组后,窗口中仍没有重复元素,返回 False

python 代码:

```python
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
window = set() # 使用一个集合表示窗口

for i in range(len(nums)):

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) 更合适

if nums[i] in window:
return True

window.add(nums[i])

if len(window) > k:
window.remove(nums[i-k]) # 移出最先加进来的元素

return False
```
42 changes: 42 additions & 0 deletions SlidingWindow/04-type-3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
## 类型3:使用对撞指针的滑动窗口

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

说实话我觉得这个应该不太算滑动窗口。。。?
这个是 meet in the middle 吧,或者「双指针」也行,和滑动窗口还是有点区别的


「对撞指针」是双指针的一种变形,一个指针指向数组头,一个指向数组尾,两指针往相反的方向移动。而指针间可以被认为是一个窗口,这类滑动窗口也常常带有一点贪心算法的思想,例题如下:

「LeetCode」 第 11 题:[盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/)

> 给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
>
> 示例:
>
> 输入:[1,8,6,2,5,4,8,3,7]
> 输出:49

假设给定的数组为 `height` ,则示例中的输出则可解释为 `49 = min(height[1], height[8]) * (8-1)`

### 思路

根据「木桶原理」,我们知道决定一个容器能盛多少水的因素有两个,一个是容器本身有多大,二是最短的那块木板有多长。而在这道题,数组中的元素值代表木板长度,两元素间的间距代表容器本身的大小,即窗口的大小。

可以发现,我们虽然不知道木板最长是多少,但窗口可以有多大是知道的——即数组长度 - 1那么大。因此我们不妨在一开始就把窗口设为最大,每次判断当前木板的长度和窗口大小能盛多少水,再逐渐将窗口缩小。至此,代码也就呼之欲出了。

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,并找到两块符合窗口要求的最长的木板」,但事实上这题的思路不是这个。


python代码:

```python
class Solution:
def maxArea(self, height: List[int]) -> int:
lp = 0 # 左指针,指向左侧的木板
rp = len(height) - 1 # 右指针,指向右侧的木板
res = 0 # 能容纳的最大水量

while lp < rp:
# 每次计算当前窗口和木板长度能容纳的水量,并更新能容纳的最大水量
temp = min(height[lp], height[rp]) * (rp-lp)
res = max(res, temp)
# 不断更换长度相对短的那个木板
if height[lp] < height[rp]:
lp += 1
else:
rp -= 1

return res
```
3 changes: 3 additions & 0 deletions SlidingWindow/05-summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 总结

其实滑动窗口问题还是以类型 1 居多,重点在于思考左右指针的具体含义、窗口状态的具体含义,以及窗口要如何变化,变化条件又是什么。明白了这些,也就彻底理解了滑动窗口。

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

对的,所以我觉得这里可以总结一个类型 1 的模板出来

8 changes: 8 additions & 0 deletions SlidingWindow/06-practices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## 精选例题
Copy link
Contributor

@liweiwei1419 liweiwei1419 Apr 20, 2020

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 个不同整数的子数组(困难)


| 题目 | 提示 |
| :----------------------------------------------------------: | :--------------------------------------: |
| [3.无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) | 非常经典的一道滑动窗口问题,建议反复理解 |
| [438. 找到字符串中所有字母异位词](https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/) | 「无重复字符的最长子串」的变形 |
| [76. 最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/) | 「无重复字符的最长子串」的进阶,相对较难 |

Binary file added SlidingWindow/示意图.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 13 additions & 6 deletions _sidebar.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
- 二分查找
- [简介](/BinarySearch/01-introduction.md)
- [模板一](/BinarySearch/02-template-1.md)
- [模板二](/BinarySearch/03-template-2.md)
- [模板三](/BinarySearch/04-template-3.md)
- [例题](/BinarySearch/05-examples.md)
- [练习](/BinarySearch/06-practices.md)
- [简介](./BinarySearch/01-introduction.md)
- [模板一](./BinarySearch/02-template-1.md)
- [模板二](./BinarySearch/03-template-2.md)
- [模板三](./BinarySearch/04-template-3.md)
- [例题](./BinarySearch/05-examples.md)
- [练习](./BinarySearch/06-practices.md)
- 滑动窗口
- [简介](./SlidingWindow/01-introduction.md)
- [类型一](./SlidingWindow/02-type-1.md)
- [类型二](./SlidingWindow/03-type-2.md)
- [类型三](./SlidingWindow/04-type-3.md)
- [总结](./SlidingWindow/05-summary.md)
- [练习题](./SlidingWindow/06-practices.md)