|
| 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 | + |
| 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