双指针法
所谓双指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向或者相反方向的指针进行扫描,从而达到相应的目的。
1、对撞指针
对撞指针是指在有序数组中,将指向最左侧的索引定义为左指针(left)
,最右侧的定义为右指针(right)
,然后从两头向中间进行数组遍历。
对撞数组适用于有序数组,也就是说当你遇到题目给定有序数组时,应该第一时间想到用对撞指针解题。
1) 求和问题
对于这种问题,常见的算法思路不外乎遍历,回溯,但这里,双指针遍历法是一个很有效的方法。具体思路是:初始化两个指针,一个指向数组的第一个元素,另外一个指向数组的最后一个元素,在两个指针相遇之前,指针1只能向前移动,指针2 只能向后移动。比较当前两个指针所指元素和与给定数字的大小,如果和较大,指针2向后移动,如果和较小,指针1向前移动。最终的结果是找到两个满足条件的数或者不存在这样的两个数字。
A. 两数之和问题
问题
给定一个整数数组nums,返回数组中两个数之和为目标值target的两个整数的下标
思路
- 对数组进行排序
- 初始化两个指针,一个指向头L,一个指向尾R
- 判断nums[L] + nums[R] 与 target 的关系
- 若nums[L] + nums[R] > target,则说明nums[R]比较大,故R—。若nums[L] + nums[R] < target,则说明nums[L]比较小,故L++。
- 直到L>R结束
1 | def twoSum(nums: List[int], target: int) -> List[int]: |
B. 三数之和问题
问题
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
思路
- 对数组进行排序
- 首先按照顺序选择第一个数字nums[i],之后利用双指针法得到之后的两个数字
- 遍历排序后数组:
- 若 nums[i]>0:因为已经排序好,所以后面不可能有三个数加和等于 0,直接返回结果。
- 对于重复元素:跳过,避免出现重复解
- 令左指针 L=i+1,右指针 R=n-1,当 L<R 时,执行循环:
- 当 nums[i]+nums[L]+nums[R]==0,执行循环,判断左界和右界是否和下一位置重复,去除重复解。并同时将 L,R移到下一位置,寻找新的解
- 若和大于 0,说明 nums[R]太大,R左移
- 若和小于 0,说明 nums[L]太小,L右移
1 | def threeSum(nums: List[int]) -> List[List[int]]: |
C. 最接近的三数之和
问题
给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。
思路
该题的思路与三数之和几乎一致,只是这次是进行比较
- 对数组进行排序
- 首先按照顺序选择第一个数字nums[i],之后利用双指针法得到之后的两个数字
- 使用res记录答案
- 遍历排序后数组:
- 令左指针 L=i+1,右指针 R=n-1,当 L<R 时,执行循环:
- 令tmp=nums[i]+nums[R]+nums[L]
- 若abs(tmp-target) < abs(res-target),说明我们找到了更有的结果,替换res
- 若 tmp == target,说明,该结果最合适,最直接返回
- 若tmp>target,说明nums[R]太大,要让其变小,才有可能找到最适合的结果,R左移
- 若tmp<target,说明nums[L]太小,要让其变大,才有可能找到最适合的结果,L右移
- 令左指针 L=i+1,右指针 R=n-1,当 L<R 时,执行循环:
1 | def threeSumClosest(nums: List[int], target: int) -> int: |
D. 四数之和
问题
给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
思路
与三数之和问题一致,只是将外层遍历改成两层,此处不放代码了。
2)盛最多水的容器
问题
给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
思路
- 设S(i,j)为i与j之间的水的面积,h(i)表示第i个点的高度,res代表面积最大的值
- 定义左指针为L=0,右指针R=n-1
- 每次将L,R中较小的一个往里面移动,计算面积,更新res,直到L>R结束
PS:该思路的难点在于,该解法是都一定将所有的情况都遍历到
- 我们从(0,n-1)开始计算,如果h(0)比较小,那么易知,S(0,j) (其中0<j<n-1)都小于S(0,n-1),有了这个知识,我们就可以进行如下的描述
- 如果将所有的面积值,列成一个矩阵(n+1行,n+1列),行代表左边,列代表右边
- 如果h(0)比较小,就让我们将0这一行的情况,都知道了,即这一行的最大值为(0,n-1)这个点,这一行的其他地方,我们就不需要再遍历了。之后,就到了下一行的(1,n-1)。如果是h(n-1)较小,那么在n-1这一列的情况就知道了。
- 于是,我们可以知道我们的算法是一列一列的或者一行一行的来排除情况
- 我们总共遍历n+1次,因此,可以将所有的面积值都考虑到
1 | def maxArea(height: List[int]) -> int: |
3)快速排序法类方法
此类方法,使用了双指针,从两侧进行遍历数组,一旦两个元素符合条件,则进行交换,来达到目的
A. 快速排序
快速排数采用了双指针的思想,时间复杂度为$ O(nlog_2n)$ 。
问题
对数组nums进行排序
思路
- 首先选择数组nums[0]为基准,假设值为H,置左指针L=1,右指针R=n-1
- 右侧先进行移动,找到第一个小于H的值,之后左侧进行移动,找到第一个大于H的值,将两个值进行交换。
- 交换完成后,R,L继续这样移动,直到L==R,将H与nums[L]交换。首先移动右侧是为了在L等于R的位置上是小于H的数。
- 交换完成后,在H的左侧都是小于H的数字,右侧都是大于H的数字
- 再分别对H的左侧和H的右侧进行快速排序,即可得到最后的结果
1 | def quick_sort(nums: List[int],left:int,right:int)-> List[int]: |
B. 奇偶排序
问题
给定一个数组,数组中元素有奇数有偶数。要求对数组进行处理,使得数组的左边为奇数,右边为偶数
思路
奇偶排序与快速排序的方法类似,只是将快速排序的条件更改成了奇数和偶数的不同
2、快慢指针
快慢指针也是双指针,但是两个指针从同一侧开始遍历数组,将这两个指针分别定义为快指针(fast)
和慢指针(slow)
,两个指针以不同的策略移动,直到两个指针的值相等(或其他特殊条件)为止,如fast每次增长两个,slow每次增长一个。
1)链表中点问题
对于单链表求中间元素的问题,经典的作法是采用两个指针,初始化都指向链表表头,移动开始时,快指针移动两步,慢指针移动一步。当快指针的next为null或者快指针为null的时候,慢指针所指的就是单链表的中间元素(注意奇数个元素和偶数个元素的处理)
2)删除链表的倒数第N个节点
问题
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
思路
高阶方法:我们可以通过两个指针,依次遍历来找到倒数第N个节点,一个快节点,一个慢节点,慢节点比快节点慢n+1步,当第一个节点遍历完后,第二个节点指向的就是倒数第N+1个节点
- 初始化两个指针,fast,slow 都指向头
- 使用fast指针从头遍历链表,使用i记录快指针的步数
- 当i达到临界值,即当i>n时,表示fast比slow多走了n+1步了,这个时候slow指针也开始移动
- 直到遍历结束,此时slow指针指向倒数第N+1个节点,i记录了链表的总长度
- 删除第N个节点即可
1 | def removeNthFromEnd(head: ListNode, n: int) -> ListNode: |
3)删除排序数组中的重复项
问题
给定一个有序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
思路
- 既然不能使用额外的数组空间,那么我们就将不同的元素依次放到数组前面去
- 初始化两个指针,快指针fast和慢指针slow
- fast元素用来找到指针中的不同元素,slow指针用来将这些不同元素放到特定位置
- 遍历一遍数组即可
1 | def removeDuplicates(nums: List[int]) -> int: |
参考链接