常见编程模式之双指针

时间:2022-07-23
本文章向大家介绍常见编程模式之双指针,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

2. 双指针(Two Pointers)

基本原理及应用场景

双指针模式指使用两个一前一后的指针遍历数据结构,直到某个指针触发停止条件。该模式常用于在有序数组或链表中搜索元素对。使用双指针的好处在于和单指针相比,不用去连续遍历整个数组来找出答案,可以带来更好的时间或空间复杂度。

在以下场景中,我们可能会用到双指针:

  • 题目涉及「有序」数组或链表,成对元素的集合、三元组甚至是子数组
  • 题目要求去匹配一个「目标」值或是去除重复

经典例题

1. 两数之和(Easy)

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那「两个」整数,并返回他们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

「示例」

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

这道题官方题解的解法是通过构造数据结构表示数组中元素与其索引的对应关系,然后检查数组中是否存在目标元素即可。对于 python,可以使用「字典」进行表示。该方法的时间复杂度和空间复杂度均为

O(N)

class Solution(object):
    def twoSum(self, nums, target):
        buff_dict = {}
        for i,num in enumerate(nums):
            complement = target - num
            if complement in buff_dict: # 判断键是否存在于字典中
                return [buff_dict[complement], i]
            buff_dict[num] = i

由于给定的数组「有序」,我们还可以使用「双指针」来解决这个问题。将双指针初始化在数组的两侧,计算元素之和,如果大于目标值则将右指针左移,小于目标值则将左指针右移,如下图所示:

该方法对应的 python 实现如下,时间复杂度为

O(N)

,空间复杂度缩减为

O(1)

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        left, right = 0, len(nums) - 1
        while left < right:
            current_sum = nums[left] + nums[right]
            if current_sum == target:
                return [left, right]    
            if target > current_sum:
                left += 1
            else: 
                right -= 1
        return [-1, -1]

15. 三数之和(Medium)

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

「示例」

给定数组 nums = [-1, 0, 1, 2, -1, -4],

满足要求的三元组集合为:
[
  [-1, 0, 1],
  [-1, -1, 2]
]

这道题也是双指针的经典题目。首先为了保证三元组不重复,我们先对数组进行排序,保证三元组中的数按从小到大的顺序排列。然后开始通过循环对数组进行遍历,找出可行解。这里为了减少复杂度,在第一个元素固定的情况下,对于后两个元素我们可以使用双指针,如果两者之和大于目标值,则将左移右指针,如果小于目标值,则右移左指针。注意在移动时要去除重复(即相邻两次移动对应的值不能相同)。该方法的 python 实现如下:

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        n = len(nums)
        if not nums or n < 3: return []
        nums.sort() # 排序数组
        ans = list()

        for first in range(n):
            if nums[first] > 0: # 如果第一个元素大于0,由于数组有序,后面必不存在可行解,直接返回即可
                return ans
            if first > 0 and nums[first] == nums[first - 1]:
                continue # 如果和上一次相同则跳过
            second = first + 1
            third = n - 1
            target = -nums[first]
            while second < third:
                if nums[second] + nums[third] == target:
                    while second < third and nums[second] == nums[second + 1]: 
                        second += 1 # 重复则跳过,下同(注意只需要在满足条件时跳过即可)
                    while second < third and nums[third] == nums[third - 1]:
                        third -= 1
                    ans.append([nums[first], nums[second], nums[third]])
                    second += 1 # 添加完可行解后继续遍历
                    third -= 1
                elif nums[second] + nums[third] < target:
                    second += 1
                else:
                    third -= 1
        return ans

42. 接雨水(Hard)

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

这道题可以通过多种方法求解,这里仅介绍较为巧妙的双指针解法。首先需要明确,对于某一列来说,其能接雨水的充要条件是该列的高度小于其左侧和右侧最大高度的最小值,以第五列为例,其高度为 1,而左侧最大高度为 2,右侧最大高度为 3,较小值为 2,则将其与当前列高度比较,由于当前列高度较小,所以可以装下 2-1=1 个单位的雨水。

基于上述思路,对于当前列来说,我们只需要找出其左侧和右侧的最大高度(不包括当前列),即可计算其能接的雨水量。基于双指针的思想,我们设置一左一右两个指针,分别从左到右和从右到左遍历找出最大高度,记为 left_maxright_max,如下图所示(考自 LeetCode 评论区大神):

                                  right_max
 left_max                             __
   __                                |  |
  |  |__   __??????????????????????  |  |
__|     |__|                       __|  |__
        left                      right

可以看到,如果 left_max < right_max,则对于左指针 left对应的列而言,其左边的最大高度必为 left_max,而右边的最大高度必大于等于 right_max(因为中间部分未知),所以偏小的最大高度一定位于左侧(left_max < right_max <= true_right_max),这时我们处理左指针,并将其右移。反之,我们则需要去处理右指针,并将其左移。该思路对应的 python 实现如下:

class Solution:
    def trap(self, height: List[int]) -> int:
        left = 0
        right = len(height) - 1
        left_max = right_max = 0 # 表示左指针左侧的最大高度和右指针右侧的最大高度
        ans = 0
        while left <= right:
            if left_max < right_max:
                ans += max(0, left_max - height[left])
                left_max = max(left_max, height[left]) # 计算下一次循环用的最大高度值
                left += 1
            else:
                ans += max(0, right_max - height[right])
                right_max = max(right_max, height[right])
                right -= 1
        return ans

其他类似的题目列表:

  • LeetCode 16-「最接近的三数之和」(Medium)
  • LeetCode 26-「删除排序数组中的重复项」(Easy)
  • LeetCode 27-「移除元素」(Easy)
  • LeetCode 75-「颜色分类」(Medium)
  • LeetCode 125-「验证回文串」(Easy)
  • LeetCode 259-「较小的三数之和」(Medium)
  • LeetCode 349-「两个数组的交集」(Easy)
  • LeetCode 350-「两个数组的交集 II」(Easy)
  • LeetCode 680-「验证回文字符串 II」(Easy)
  • LeetCode 713-「乘积小于 K 的子数组」(Medium)
  • LeetCode 977-「有序数组的平方」(Easy)