Skip to content

Commit 30904ef

Browse files
committed
Update 01.Array-Two-Pointers.md
1 parent 613abbe commit 30904ef

1 file changed

Lines changed: 351 additions & 0 deletions

File tree

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
## 1. 双指针简介
2+
3+
> 双指针(Two Pointers):指的是在遍历元素的过程中,不是使用单个指针进行访问,而是使用两个指针进行访问,从而达到相应的目的。如果两个指针方向相反,则称为「对撞时针」。如果两个指针方向相同,则称为「快慢指针」。如果两个指针分别属于不同的数组 / 链表,则称为「分离双指针」。
4+
5+
在数组的区间问题上,暴力算法的时间复杂度往往是 $O(n^2)$。而双指针利用了区间「单调性」的性质,从而将时间复杂度降到了 $o(n)$。
6+
7+
## 2. 对撞指针
8+
9+
> 对撞指针:指的是两个指针 `left``right` 分别指向序列第一个元素和最后一个元素,然后 `left` 指针不断递增,`right` 不断递减,直到两个指针的值相撞(即 `left == right`),或者满足其他要求的特殊条件为止。
10+
11+
### 2.1 对撞指针求解步骤
12+
13+
- 使用两个指针 `left``right``left` 指向序列第一个元素,即:`left = 0``right` 指向序列最后一个元素,即:`right = len(nums) - 1`
14+
- 在循环体中将左右指针相向移动,当满足一定条件时,将左指针右移,`left += 1`。当满足另外一定条件时,将右指针左移,`right -= 1`
15+
- 直到两指针相撞(即 `left == right`),或者满足其他要求的特殊条件时,跳出循环体。
16+
17+
### 2.2 对撞指针伪代码模板
18+
19+
```Python
20+
left = 0
21+
right = len(nums) - 1
22+
23+
while left < right:
24+
if 满足要求的特殊条件:
25+
return 符合条件的值
26+
elif 一定条件 1:
27+
left += 1
28+
elif 一定条件 2:
29+
right -= 1
30+
31+
return 没找到 或 对应值
32+
```
33+
34+
### 2.3 对撞指针适用范围
35+
36+
对撞指针一般用来解决有序数组或者字符串问题:
37+
38+
- 查找有序数组中满足某些约束条件的一组元素问题:比如二分查找、数字之和等问题。
39+
- 字符串反转问题:反转字符串、回文数、颠倒二进制等问题。
40+
41+
下面我们根据具体例子来讲解如何使用对撞指针来解决问题。
42+
43+
### 2.4 [两数之和 II - 输入有序数组](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/)
44+
45+
#### 2.4.1 题目大意
46+
47+
给定一个升序排列的整数数组:`numbers` 和一个目标值 `target`
48+
49+
要求:从数组中找出满足相加之和等于 `target` 的两个数,并返回两个数在数组中下的标值。
50+
51+
注意:数组下标从 `1` 开始计数。
52+
53+
#### 2.4.2 解题思路
54+
55+
这道题如果暴力遍历数组,从中找到相加之和等于 `target` 的两个数,时间复杂度为 `O(n^2)`,可以尝试一下。
56+
57+
```Python
58+
class Solution:
59+
def twoSum(self, numbers: List[int], target: int) -> List[int]:
60+
size = len(numbers)
61+
for i in range(size):
62+
for j in range(i + 1, size):
63+
if numbers[i] + numbers[j] == target:
64+
return [i + 1, j + 1]
65+
return [-1, -1]
66+
```
67+
68+
结果不出意外的超时了。所以我们要想办法减少时间复杂度。
69+
70+
因为数组是有序的,所以我们可以考虑使用双指针来减少时间复杂度。具体做法如下:
71+
72+
- 使用两个指针 `left``right``left` 指向数组第一个值最小的元素位置,`right` 指向数组值最大元素位置。
73+
- 判断两个位置上的元素的和与目标值的关系。
74+
- 如果元素和等于目标值,则返回两个元素位置。
75+
- 如果元素和大于目标值,则让 `right` 左移,继续检测。
76+
- 如果元素和小于目标值,则让 `left` 右移,继续检测。
77+
- 直到 `left``right` 移动到相同位置停止检测。
78+
- 如果最终仍没找到,则返回 `[-1, -1]`
79+
80+
#### 2.4.3 代码
81+
82+
```Python
83+
class Solution:
84+
def twoSum(self, numbers: List[int], target: int) -> List[int]:
85+
left = 0
86+
right = len(numbers) - 1
87+
while left < right:
88+
total = numbers[left] + numbers[right]
89+
if total == target:
90+
return [left + 1, right + 1]
91+
elif total < target:
92+
left += 1
93+
else:
94+
right -= 1
95+
return [-1, -1]
96+
```
97+
98+
### 2.5 [验证回文串](https://leetcode-cn.com/problems/valid-palindrome/)
99+
100+
#### 2.5.1 题目大意
101+
102+
给定一个字符串 `s`
103+
104+
要求:判断是否为回文串(只考虑字符串中的字母和数字字符,并且忽略字母的大小写)。
105+
106+
- 回文串:正着读和反着读都一样的字符串。
107+
108+
#### 2.5.2 解题思路
109+
110+
使用对撞指针求解。具体步骤如下:
111+
112+
- 使用两个指针 `left``right``left` 指向字符串开始位置,`right` 指向字符串结束位置。
113+
- 判断两个指针对应字符是否是字母或数字。 通过 `left` 右移、`right` 左移的方式过滤掉字母和数字以外的字符。
114+
- 然后判断 `s[start]` 是否和 `s[end]` 相等(注意大小写)。
115+
- 如果相等,则将 `left` 右移、`right` 左移,继续进行下一次过滤和判断。
116+
- 如果不相等,则说明不是回文串,直接返回 `False`
117+
- 如果遇到 `left == right`,跳出循环,则说明该字符串是回文串,返回 `True`
118+
119+
#### 代码
120+
121+
```Python
122+
class Solution:
123+
def isPalindrome(self, s: str) -> bool:
124+
left = 0
125+
right = len(s) - 1
126+
127+
while left < right:
128+
if not s[left].isalnum():
129+
left += 1
130+
continue
131+
if not s[right].isalnum():
132+
right -= 1
133+
continue
134+
135+
if s[left].lower() == s[right].lower():
136+
left += 1
137+
right -= 1
138+
else:
139+
return False
140+
return True
141+
```
142+
143+
### 2.6 [盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/)
144+
145+
#### 2.6.1 题目大意
146+
147+
给定 `n` 个非负整数 $a_1,a_2, ...,a_n$,每个数代表坐标中的一个点 $(i, a_i)$。在坐标内画 `n` 条垂直线,垂直线 `i` 的两个端点分别为 $(i, a_i)$ 和 $(i, 0)$。
148+
149+
要求:找出其中的两条垂直线,使得它们与 `x` 轴共同构成的容器可以容纳最多的水。
150+
151+
示例:
152+
153+
![](https://aliyun-lc-upload.oss-cn-hangzhou.aliyuncs.com/aliyun-lc-upload/uploads/2018/07/25/question_11.jpg)
154+
155+
- 输入:`[1,8,6,2,5,4,8,3,7]`
156+
- 输出:`49`
157+
- 解释:图中垂直线代表输入数组 `[1,8,6,2,5,4,8,3,7]`。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 `49`
158+
159+
#### 2.6.2 解题思路
160+
161+
从示例中可以看出,如果确定好左右两端的直线,容纳的水量是由 `左右两端直线中较低直线的高度 * 两端直线之间的距离 ` 所决定的。所以我们应该使得 **较低直线的高度尽可能的高**,这样才能使盛水面积尽可能的大。
162+
163+
可以使用双指针求解。移动较低直线所在的指针位置,从而得到不同的高度和面积,最终获取其中最大的面积。具体做法如下:
164+
165+
- 使用两个指针 `left``right``left` 指向数组开始位置,`right` 指向数组结束位置。
166+
- 计算 `left``right` 所构成的面积值,同时维护更新最大面积值。
167+
- 判断 `left``right` 的高度值大小。
168+
- 如果 `left` 指向的直线高度比较低,则将 `left` 指针右移。
169+
- 如果 `right` 指向的直线高度比较低,则将 `right` 指针左移。
170+
- 如果遇到 `left == right`,跳出循环,最后返回最大的面积。
171+
172+
#### 2.6.3 代码
173+
174+
```Python
175+
class Solution:
176+
def maxArea(self, height: List[int]) -> int:
177+
left = 0
178+
right = len(height) - 1
179+
ans = 0
180+
while left < right:
181+
area = min(height[left], height[right]) * (right-left)
182+
ans = max(ans, area)
183+
if height[left] < height[right]:
184+
left += 1
185+
else:
186+
right -= 1
187+
return ans
188+
```
189+
190+
## 3. 快慢指针
191+
192+
> 快慢指针:指的是两个指针从同一侧开始遍历序列,且移动的步长一个快一个慢。移动快的指针被称为 「快指针(fast)」,移动慢的指针被称为「慢指针(slow)」。两个指针以不同速度、不同策略移动,直到快指针移动到数组尾端,或者两指针相交,或者满足其他特殊条件时为止。
193+
194+
### 3.1 快慢指针求解步骤
195+
196+
- 使用两个指针 `slow``fast``slow` 一般指向序列第一个元素,即:`slow = 0``fast` 一般指向序列第二个元素,即:`fast = 1`
197+
- 在循环体中将左右指针向右移动。当满足一定条件时,将慢指针右移,即 `slow += 1`。当满足另外一定条件时(也可能不需要满足条件),将快指针右移,即 `fast += 1`
198+
- 到快指针移动到数组尾端(即 `fast == len(nums) - 1`),或者两指针相交,或者满足其他特殊条件时跳出循环体。
199+
200+
### 3.2 快慢指针伪代码模板
201+
202+
```Python
203+
slow = 0
204+
fast = 1
205+
while 没有遍历完:
206+
if 满足要求的特殊条件:
207+
slow += 1
208+
fast += 1
209+
return 合适的值
210+
```
211+
212+
### 3.3 快慢指针适用范围
213+
214+
快慢指针一般用于处理数组中的移动、删除元素问题,或者链表中的判断是否有环、长度问题。关于链表相关的双指针做法我们到链表章节再详细讲解。
215+
216+
下面我们根据具体例子来讲解如何使用快慢指针来解决问题。
217+
218+
### 3.4 [删除有序数组中的重复项](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/)
219+
220+
#### 3.4.1 题目大意
221+
222+
给定一个有序数组 `nums`
223+
224+
要求:删除数组 `nums` 中的重复元素,使每个元素只出现一次。并输出去除重复元素之后数组的长度。
225+
226+
注意:不能使用额外的数组空间,在原地修改数组,并在使用 $O(1)$ 额外空间的条件下完成。
227+
228+
#### 3.4.2 解题思路
229+
230+
因为数组是有序的,那么重复的元素一定会相邻。
231+
232+
删除重复元素,实际上就是将不重复的元素移到数组左侧。考虑使用双指针。具体算法如下:
233+
234+
1. 定义两个快慢指针 `slow``fast`。其中 `slow` 指向去除重复元素后的数组的末尾位置。`fast` 指向当前元素。
235+
2.`slow` 在后, `fast` 在前。令 `slow = 0``fast = 1`
236+
3. 比较 `slow` 位置上元素值和 `fast` 位置上元素值是否相等。
237+
- 如果不相等,则将 `slow` 后移一位,将 `fast` 指向位置的元素复制到 `slow` 位置上。
238+
4.`fast` 右移 `1` 位。
239+
240+
- 重复上述 3 ~ 4 步,直到 `fast` 等于数组长度。
241+
- 返回 `slow + 1` 即为新数组长度。
242+
243+
#### 3.4.3 代码
244+
245+
```Python
246+
class Solution:
247+
def removeDuplicates(self, nums: List[int]) -> int:
248+
if len(nums) <= 1:
249+
return len(nums)
250+
251+
slow, fast = 0, 1
252+
253+
while (fast < len(nums)):
254+
if nums[slow] != nums[fast]:
255+
slow += 1
256+
nums[slow] = nums[fast]
257+
fast += 1
258+
259+
return slow + 1
260+
```
261+
262+
## 4. 分离双指针
263+
264+
> 分离双指针:两个指针分别属于不同的数组 / 链表,两个指针分别在两个数组 / 链表中移动。
265+
266+
### 4.1 分离双指针求解步骤
267+
268+
- 使用两个指针 `left_1``left_2``left_1` 指向第一个数组 / 链表的第一个元素,即:`left_1 = 0``left_2` 指向第二个数组 / 链表的第一个元素,即:`left_2 = 0`
269+
- 当满足一定条件时,两个指针同时右移,即 `left_1 += 1``left_2 += 1`
270+
- 当满足另外一定条件时,将 `left_1` 指针右移,即 `left_1 += 1`
271+
- 当满足其他一定条件时,将 `left_2` 指针右移,即 `left_2 += 1`
272+
- 当其中一个数组 / 链表遍历完时或者满足其他特殊条件时跳出循环体。
273+
274+
### 4.2 分离双指针伪代码模板
275+
276+
```Python
277+
left_1 = 0
278+
left_2 = 0
279+
280+
while left_1 < len(nums1) and left_2 < len(nums2):
281+
if 一定条件 1:
282+
left_1 += 1
283+
left_2 += 2
284+
elif 一定条件 2:
285+
left_1 += 1
286+
elif 一定条件 3:
287+
left_2 += 1
288+
```
289+
290+
### 4.3 分离双指针使用范围
291+
292+
分离双指针一般用于处理有序数组合并,求交集、并集问题。下面我们根据具体例子来讲解如何使用分离双指针来解决问题。
293+
294+
### 4.4 [两个数组的交集](https://leetcode-cn.com/problems/intersection-of-two-arrays/)
295+
296+
#### 4.4.1 题目大意
297+
298+
给定两个数组 `nums1``nums2`
299+
300+
要求:编写一个函数来计算它们的交集。重复元素只计算一次。
301+
302+
#### 4.4.2 解题思路
303+
304+
使用分离双指针求解,具体步骤如下:
305+
306+
- 对数组 `nums1``nums2` 先排序。
307+
- 使用两个指针 `left_1``left_2``left_1` 指向第一个数组的第一个元素,即:`left_1 = 0``left_2` 指向第二个数组的第一个元素,即:`left_2 = 0`
308+
- 如果 `nums1[left_1]` 等于 `nums2[left_2]`,则将其加入答案数组(注意去重),并将 `left_1``left_2` 右移。
309+
- 如果 `nums1[left_2]` 小于 `nums2[left_2]`,则将 `left_1` 右移。
310+
- 如果 `nums1[left_2]` 大于 `nums2[left_2]`,则将 `left_2` 右移。
311+
- 最后输出答案数组。
312+
313+
#### 4.4.3 代码
314+
315+
```Python
316+
class Solution:
317+
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
318+
nums1.sort()
319+
nums2.sort()
320+
321+
left_1 = 0
322+
left_2 = 0
323+
res = []
324+
while left_1 < len(nums1) and left_2 < len(nums2):
325+
if nums1[left_1] == nums2[left_2]:
326+
if nums1[left_1] not in res:
327+
res.append(nums1[left_1])
328+
left_1 += 1
329+
left_2 += 1
330+
elif nums1[left_1] < nums2[left_2]:
331+
left_1 += 1
332+
elif nums1[left_1] > nums2[left_2]:
333+
left_2 += 1
334+
return res
335+
```
336+
337+
## 5. 双指针总结
338+
339+
双指针分为「对撞指针」、「快慢指针」、「分离双指针」。
340+
341+
- 对撞指针:两个指针方向相反。适合解决查找有序数组中满足某些约束条件的一组元素问题、字符串反转问题。
342+
- 快慢指针:两个指针方向相同。适合解决数组中的移动、删除元素问题,或者链表中的判断是否有环、长度问题。
343+
- 分离双指针:两个指针分别属于不同的数组 / 链表。适合解决有序数组合并,求交集、并集问题。
344+
345+
## 参考资料
346+
347+
- 【博文】[双指针算法之快慢指针 (yanyusoul.com)](https://yanyusoul.com/blog/cs/algorithms_fast-slow-points/)
348+
- 【博文】[双指针算法各类基础题型总结 - 掘金](https://juejin.cn/post/6855129006451687431)
349+
- 【博文】[双指针 - 力扣加加 - 努力做西湖区最好的算法题解](https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/91/two-pointers#zuo-you-duan-dian-zhi-zhen)
350+
- 【博文】[LeetCode分类专题(四)——双指针和滑动窗口1 - iwehdio - 博客园](https://www.cnblogs.com/iwehdio/p/14434988.html)
351+
- 【博文】[双指针算法各类基础题型总结 - 掘金](https://juejin.cn/post/6855129006451687431)

0 commit comments

Comments
 (0)