七十三、从三数之和探究双指针思想

@Author:Runsen

编程的本质来源于算法,而算法的本质来源于数学,编程只不过将数学题进行代码化。 ---- Runsen

双指针

双指针是一种解决问题的技巧或者思维方式,指在访问一个序列中的数据时使用两个指针进行扫描,两个指针可以是同向的,也可以是反向的。

我们的关注点可以是这两个指针指向的两个元素本身,也可以是两个指针中间的区域。二分法的思想基于这种左右指针的实现。

双指针是一种思想,一种技巧或一种方法,并不是什么特别具体的算法。在区间问题上,暴力的做法的复杂度往往达到 O ( n 2 ) O(n^2) O(n2)复杂度,而双指针的思想挖掘区间“单调”性质将复杂度降到 O ( n ) O(n) O(n)

常用的双指针思想有:快慢指针、碰撞指针、滑动窗口等。

快慢指针:快慢指针按照某种规律运动。例如,设置快慢两个指针,快指针先移动距离,慢指针跟快指针同时移动,这样快慢指针之间总是保持一段相同的距离。常见的应用场景主要出现在链表中,如:链表的环的判断,求链表的中间节点等操作。

碰撞指针:在排序好的数组中,设置头指针和尾指针,按照规则,分别向中间靠拢。常见的应用场景主要出现在有序数组中:数组的和,二分查找等。这里需要强调的是:对于碰撞指针要用于已排序的区间。

滑动窗口:两个指针,一前一后组成滑动窗口,并计算滑动窗口中的元素的问题。常见问题:字符串匹配问题等,用来解决一些查找满足一定条件的连续区间求值或长度的问题。

LeetCode 第 15题:三数之和

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

注意:答案中不可以包含重复的三元组。

示例: 
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
 [-1, 0, 1],
 [-1, -1, 2]
]

在Leetcode上第一题是两数之和,使用Hashmap储存,时间复杂度是 O ( n ) O(n) O(n)

如果三数之和使用该方法,时间复杂度是 O ( n 2 ) O(n^2) O(n2)。题目中说的不可以包含重复的三元组,然后在去去重,这样是非常费时的,很容易超时但是过高的时间复杂度导致代码编译效率极差,我记得很清楚Leetcode不给予通过。这种方法是在面试中实在想不出其他解法时的选择…

双指针思路:采取左右两个指针代替两个for循环,在第一层循环下调节指针的位置,设置判断条件就可以排除很多重复项和不满足条件的组合,最终得到满足题目的三元组,具体的伪代码大致如下:

function fn (list) {
  var left = 0;
  var right = list.length - 1;

  //遍历数组
  while (left <= right) {
    left++;
    // 一些条件判断 和处理
    ... ...
    right--;
  }
}

由于本提中给出的数组是未排序,且有重复数据的情况,所以首先需要做排序和去重处理

下面使用排序 + 双指针方法解决:

看到这张概念图后,是不是已经有内味了?

  • 首先进行数组排序,时间复杂度 O(nlogn)
  • 对数组nums进行遍历,每遍历一个值利用其下标 i,形成一个固定值 nums[i]
  • 如果 nums[i]大于0, 则三数之和必然无法等于0,直接结束循环
  • 如果 nums[i] == nums[i-1],则说明该数字重复,会导致结果重复,所以应该跳过
  • 再使用前指针指向 l = i + 1处,后指针指向r = nums.length - 1,也就是结尾处
  • 根据 three_sum = nums[i] + nums[l] + nums[r]结果,判断 three_sum 与 0 的大小关系,满足则添加进入结果,此时 l+=1r-=1。 如果 three_sum < 0,则l+=1, 如果 three_sum > 0, 则 `r-=1``
  • three_sum === 0 的时候还要考虑结果重复的情况
  • nums[l] == nums[l+1] 则会导致结果重复,应该跳过,l++
  • nums[r] == nums[r-1] 则会导致结果重复,应该跳过,r–
  • 总时间复杂度: O ( n l o g n ) + O ( n 2 ) = O ( n 2 ) O(nlogn) + O(n^2) = O(n^2) O(nlogn)+O(n2)=O(n2)

具体查看代码:

class Solution:
    def threeSum(nums):
    nums.sort()
    # [-4, -1, -1, 0, 1, 2]
    res_list = []
    # 头部循环查找
    for i in range(len(nums)):
        if i == 0 or nums[i] > nums[i - 1]:
            # 最左端
            l = i + 1
            # 最右端
            r = len(nums) - 1
            while l < r:  # 正在查找
                three_sum = nums[i] + nums[l] + nums[r]
                if three_sum == 0:
                    res_list.append([nums[i], nums[l], nums[r]])
                    l += 1  # 右移一位
                    r -= 1  # 左移一位
                    while l < r and nums[l] == nums[l - 1]:
                        # 从左往右,相同数值直接跳过
                        l += 1
                    while r > l and nums[r] == nums[r + 1]:
                        # 从右往左,相同数值直接跳过
                        r -= 1
                elif three_sum > 0:
                    # 大于零,右边数值大,左移
                    r -= 1
                else:
                    # 小于零,左边数值小,右移
                    l += 1
    return res_list

LeetCode 第 16题:最接近的三数之和

给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。

示例: 
输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。
提示: 
3 <= nums.length <= 10^3 
-10^3 <= nums[i] <= 10^3 
-10^4 <= target <= 10^4 

本题目因为要计算三个数,如果靠暴力枚举的话时间复杂度会到 O ( n 3 ) O(n^3) O(n3),需要降低时间复杂度,借鉴上面的三数之和,其实两道题的解决思路几乎一样,只不过这道题需要不停着记录三个数的和与 target 之间的差。

  • 首先进行数组排序,时间复杂度O(nlogn)
  • 在数组nums中,进行遍历,每遍历一个值利用其下标i,形成一个固定值nums[i]
  • 再使用前指针指向j= i + 1处,后指针指向k= nums.length - 1处,也就是结尾处
  • 根据 sum = nums[i] + nums[start] + nums[end]的结果,判断sum与目标target的距离,如果更近则更新结果ans
  • 同时判断sum与target的大小关系,因为数组有序,如果sum > targetk--,如果sum < target 则 j++,如果sum == target 则说明距离为0直接返回结果
  • 整个遍历过程,固定值为n次,双指针为n次,时间复杂度为O(n^2)
  • 总时间复杂度: O ( n l o g n ) + O ( n 2 ) = O ( n 2 ) O(nlogn) + O(n^2) = O(n^2) O(nlogn)+O(n2)=O(n2)

具体查看代码:

class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        if not nums: return 0
        if len(nums) < 3: return sum(nums)

        ans = float('inf')
        nums.sort()
        for i in range(len(nums)):
            # 优化点 1
            if i > 0 and nums[i] == nums[i-1]: 
                continue
            
            t = target - nums[i]
            j, k = i + 1, len(nums) - 1
            while j < k:
                # 优化点 2
                if t == nums[j] + nums[k]: return target

                # 当前 j、k更接近target
                if abs(t - nums[j] - nums[k]) < abs(target - ans):
                    ans = nums[i] + nums[j] + nums[k]
                # 移动j | k
                if t > nums[j] + nums[k]:
                    j += 1
                else:
                    k -= 1
        return ans

LeetCode 第 27题:移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例 1:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
注意这五个元素可为任意顺序。

首先讲讲自己做题的思路,用Python做比较简单,遍历数组,如果当前值不等于val,就是i += 1

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        i = 0
        for num in nums:
            if num != val:
                nums[i] = num
                i += 1
        return i

官方的解法是双指针,我们可以保留两个指针 i 和 j,其中 i 是慢指针,j 是快指针。

public int removeElement(int[] nums, int val) {
    int i = 0;
    for (int j = 0; j < nums.length; j++) {
        if (nums[j] != val) {
            nums[i] = nums[j];
            i++;
        }
    }
    return i;
}

人生最重要的不是所站的位置,而是内心所朝的方向。只要我在每篇博文中写得自己体会,修炼身心;在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰难,奋勇前行,不忘初心,砥砺前行,人生定会有所收获,不留遗憾 (作者:Runsen )

本文已收录 GitHub,传送门~ ,里面更有大厂面试完整考点,欢迎 Star。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RunsenLIu

顺便点一个赞

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值