【哈希表】leetcode15. 三数之和(C/C++/Java/Python/Js)--梦破碎的地方

news2024/10/9 22:24:56

leetcode15. 三数之和--梦破碎的地方

  • 1 题目
  • 2 思路
    • 2.1 哈希解法--含代码
    • 2.2 双指针
    • 2.3 去重逻辑的思考
      • 2.3.1 a的去重
      • 2.3.2 b与c的去重
  • 3 代码--双指针法
    • 3.1 C++版本
    • 3.2 C版本
    • 3.3 Java版本
    • 3.4 Python3版本
    • 3.5 JavaScript版本
  • 4 总结


用哈希表解决了两数之和 ,那么三数之和呢?

1 题目

题源链接

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

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

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

提示:

3 <= nums.length <= 3000
-105 <= nums[i] <= 105


2 思路


可以结合Carl老师的视频讲解:
梦破碎的地方!| LeetCode:15.三数之和
思路整理于Carl老师的《代码随想录》


2.1 哈希解法–含代码

两层for循环就可以确定 a 和b 的数值了,可以使用哈希法来确定 0-(a+b) 是否在 数组里出现过。
其实这个思路是正确的,但是我们有一个非常棘手的问题,就是题目中说的不可以包含重复的三元组。

把符合条件的三元组放进vector中,然后再去重,这样是非常费时的,很容易超时,也是这道题目通过率如此之低的根源所在。

去重的过程不好处理,有很多小细节,如果在面试中很难想到位。

时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。

  • 哈希法C++代码:
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        // 找出a + b + c = 0
        // a = nums[i], b = nums[j], c = -(a + b)
        for (int i = 0; i < nums.size(); i++) {
            // 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
            if (nums[i] > 0) {
                break;
            }
            if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
                continue;
            }
            unordered_set<int> set;
            for (int j = i + 1; j < nums.size(); j++) {
                if (j > i + 2
                        && nums[j] == nums[j-1]
                        && nums[j-1] == nums[j-2]) { // 三元组元素b去重
                    continue;
                }
                int c = 0 - (nums[i] + nums[j]);
                if (set.find(c) != set.end()) {
                    result.push_back({nums[i], nums[j], c});
                    set.erase(c);// 三元组元素c去重
                } else {
                    set.insert(nums[j]);
                }
            }
        }
        return result;
    }
};

2.2 双指针

其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。

而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长 。

接下来我来介绍另一个解法:双指针法,这道题目使用双指针法 要比哈希法高效一些,那么来讲解一下具体实现的思路。

动画效果如下:
在这里插入图片描述
动画展示

拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。

依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。

接下来如何移动left 和right呢?
如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以 right下标就应该向左移动,这样才能让三数之和小一些

如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。

  • 时间复杂度:O(n^2)。

2.3 去重逻辑的思考

说道去重,其实主要考虑三个数的去重。 a, b ,c, 对应的就是 nums[i],nums[left],nums[right]


2.3.1 a的去重

a 如果重复了怎么办,a是nums里遍历的元素,那么应该直接跳过去。

但这里有一个问题,是判断 nums[i] 与 nums[i + 1]是否相同,还是判断 nums[i] 与 nums[i-1] 是否相同

  • 有什么区别吗?
  • 还真有!

都是和 nums[i]进行比较,是比较它的前一个,还是比较他的后一个。

如果我们的写法是 这样:

if (nums[i] == nums[i + 1]) { // 去重操作
    continue;
}

那就我们就把 三元组中出现重复元素的情况直接pass掉了。 例如 {-1, -1 ,2}这组数据,当遍历到第一个-1 的时候,判断 下一个也是-1,那这组数据就pass了。

我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!

那么应该这么写:

if (i > 0 && nums[i] == nums[i - 1]) {
    continue;
}

这么写就是当前使用 nums[i],我们判断前一位是不是一样的元素,在看 {-1, -1 ,2} 这组数据,当遍历到 第一个 -1 的时候,只要前一位没有-1,那么 {-1, -1 ,2} 这组数据一样可以收录到 结果集里。

这是一个非常细节的思考过程。


2.3.2 b与c的去重

很多同学写本题的时候,去重的逻辑多加了 对right 和left 的去重:

while (right > left) {
    if (nums[i] + nums[left] + nums[right] > 0) {
        right--;
        // 去重 right
        while (left < right && nums[right] == nums[right + 1]) right--;
    } else if (nums[i] + nums[left] + nums[right] < 0) {
        left++;
        // 去重 left
        while (left < right && nums[left] == nums[left - 1]) left++;
    } else {
    }
}

但细想一下,这种去重其实对提升程序运行效率是没有帮助的。

拿right去重为例,即使不加这个去重逻辑,依然根据 while (right > left) 和 if (nums[i] + nums[left] + nums[right] > 0) 去完成right-- 的操作。

多加了 while (left < right && nums[right] == nums[right + 1]) right–; 这一行代码,其实就是把 需要执行的逻辑提前执行了,但并没有减少 判断的逻辑。

最直白的思考过程,就是right还是一个数一个数的减下去的,所以在哪里减的都是一样的。

所以这种去重 是可以不加的。 仅仅是 把去重的逻辑提前了而已。

3 代码–双指针法


3.1 C++版本

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        //  a = nums[i], b = nums[left], c = nums[right]
        int a, b, c, left, right;
        for (int i = 0; i < nums.size(); i++) {
            a = nums[i];
            if (a > 0)  //排序之后的第一个元素大于零,那么之后的三元组相加都不可能为0
                return result;
            if (i > 0 && nums[i] == nums[i-1])  //对a的去重
                continue;
            left = i + 1;
            right = nums.size() - 1;
            while (left < right) {
                b = nums[left];
                c = nums[right];
                if (a + b + c > 0)  //说明大了,right收缩
                    right--;
                else if (a + b + c < 0) //说明小了,left右移
                    left++;
                else {  //找到a+b+c=0的三元组之后,对b和c去重
                    result.push_back(vector<int>{a, b, c});
                    while (left < right && nums[right] == nums[right - 1])
                        right--;
                    while (left < right && nums[left] == nums[left + 1]) 
                        left++;
                    //  找到答案时,双指针同时收缩
                    right--;
                    left++;
                }
            }
        }
        return result;
    }
};

3.2 C版本

//qsort辅助cmp函数
int cmp(const void* ptr1, const void* ptr2) {
    return *((int*)ptr1) > *((int*)ptr2);
}

int** threeSum(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
    //开辟ans数组空间
    int **ans = (int**)malloc(sizeof(int*) * 18000);
    int ansTop = 0;
    //若传入nums数组大小小于3,则需要返回数组大小为0
    if(numsSize < 3) {
        *returnSize = 0;
        return ans;
    }
    //对nums数组进行排序
    qsort(nums, numsSize, sizeof(int), cmp);
    

    int i;
    //用for循环遍历数组,结束条件为i < numsSize - 2(因为要预留左右指针的位置)
    for(i = 0; i < numsSize - 2; i++) {
        //若当前i指向元素>0,则代表left和right以及i的和大于0。直接break
        if(nums[i] > 0)
            break;
        //去重:i > 0 && nums[i] == nums[i-1]
        if(i > 0 && nums[i] == nums[i-1])
            continue;
        //定义左指针和右指针
        int left = i + 1;
        int right = numsSize - 1;
        //当右指针比左指针大时进行循环
        while(right > left) {
            //求出三数之和
            int sum = nums[right] + nums[left] + nums[i];
            //若和小于0,则左指针+1(因为左指针右边的数比当前所指元素大)
            if(sum < 0)
                left++;
            //若和大于0,则将右指针-1
            else if(sum > 0)
                right--;
            //若和等于0
            else {
                //开辟一个大小为3的数组空间,存入nums[i], nums[left]和nums[right]
                int* arr = (int*)malloc(sizeof(int) * 3);
                arr[0] = nums[i];
                arr[1] = nums[left];
                arr[2] = nums[right];
                //将开辟数组存入ans中
                ans[ansTop++] = arr;
                //去重
                while(right > left && nums[right] == nums[right - 1])
                    right--;
                while(left < right && nums[left] == nums[left + 1])
                    left++;
                //更新左右指针
                left++;
                right--;
            }
        }
    }

    //设定返回的数组大小
    *returnSize = ansTop;
    *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop);
    int z;
    for(z = 0; z < ansTop; z++) {
        (*returnColumnSizes)[z] = 3;
    }
    return ans;
}

3.3 Java版本

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] > 0) 
                return result;
            if (i > 0 && nums[i - 1] == nums[i])
                continue;
            int left = i + 1, right = nums.length - 1;
            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];
                if (sum > 0)
                    right--;
                else if (sum < 0)
                    left++;
                else {
                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    while (left < right && nums[right] == nums[right - 1])  right--;
                    while (left < right && nums[left] == nums[left + 1])    left++;
                    right--;
                    left++;
                }
            }
        }
        return result;
    }
}

3.4 Python3版本

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        if len(nums) < 3:
            return []
        nums, result = sorted(nums), []
        for i in range(len(nums) - 2):
            a, l, r = nums[i], i + 1, len(nums) - 1
            if result != [] and result[-1][0] == a:
                continue
            while l < r:
                sum = a + nums[l] + nums[r]
                if sum > 0:
                    r -= 1
                elif sum < 0:
                    l += 1 
                else:
                    result.append([a, nums[l], nums[r]])
                    while l < r and nums[l] == nums[l + 1]:
                        l += 1 
                    while l < r and nums[r] == nums[r - 1]:
                        r -= 1
                    l += 1
                    r -= 1
        return result

3.5 JavaScript版本

var threeSum = function(nums) {
    const res = [], len = nums.length
    // 将数组排序
    nums.sort((a, b) => a - b)
    for (let i = 0; i < len; i++) {
        let l = i + 1, r = len - 1, iNum = nums[i]
        // 数组排过序,如果第一个数大于0直接返回res
        if (iNum > 0) return res
        // 去重
        if (iNum == nums[i - 1]) continue
        while(l < r) {
            let lNum = nums[l], rNum = nums[r], threeSum = iNum + lNum + rNum
            // 三数之和小于0,则左指针向右移动
            if (threeSum < 0) l++ 
            else if (threeSum > 0) r--
            else {
                res.push([iNum, lNum, rNum])
                // 去重
                while(l < r && nums[l] == nums[l + 1]){
                    l++
                }
                while(l < r && nums[r] == nums[r - 1]) {
                    r--
                }
                l++
                r--
            }
        }
    }
    return res
};

4 总结

双指针法,妙哉!
可以结合Carl老师的视频讲解:
梦破碎的地方!| LeetCode:15.三数之和
思路整理于Carl老师的《代码随想录》

这道题细节也很多,对于去重逻辑的思考十分重要。

By – Suki 2023/2/1

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/190888.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[Lua实战]Skynet-2.如何启动(Win10-WSL环境Ubuntu18.04)[开箱可用]

Skynet-2.如何启动Win10-WSL环境Ubuntu18.04接上文,在linux运行skynet1.WIN10-WSL1.1 用Microsoft Store安装WSL(会遇到商店下载失败等问题...)1.1.1控制面板支持Linux配置1.1.2Microsoft Store 找到 Ubuntu18.041.1.3如果遇到安装问题如图请直接跳到1.21.2 使用PowerShell工具…

概论_第7章_参数估计_点估计之极大似然估计__性质

一 性质 极大似然估计 有一个简单有用的性质&#xff1a; 如果 θ^\hat\thetaθ^ 是 θ\thetaθ的极大似然估计&#xff0c; 则对任一 θ\thetaθ的函数g(θ)g(\theta)g(θ), 其极大似然估计为 g(θ^)g(\hat\theta)g(θ^) . 该性质称为极大似然估计的不变性&#xff0c;它使…

项目代码版本控制与维护

一、版本命名规则 1.1 需求开发分支命名规则 格式&#xff1a;dev_v版本号_需求名称 案例&#xff1a;dev_v01.31_TX202301141 dev_v01.31_数字产品平台订单查询优化 1.2 测试环境发布分支命名规则 格式&#xff1a;uat_deploy 1.3 预上环境分支命名规则 格式&#xff1a…

操作系统权限提升(六)之系统错误配置-不安全的服务提权

系列文章 操作系统权限提升(一)之操作系统权限介绍 操作系统权限提升(二)之常见提权的环境介绍 操作系统权限提升(三)之Windows系统内核溢出漏洞提权 操作系统权限提升(四)之系统错误配置-Tusted Service Paths提权 操作系统权限提升(五)之系统错误配置-PATH环境变量提权 注&…

九种查找算法-B树/B+树

B树/B树 在计算机科学中&#xff0c;B树&#xff08;B-tree&#xff09;是一种树状数据结构&#xff0c;它能够存储数据、对其进行排序并允许以O(log n)的时间复杂度运行进行查找、顺序读取、插入和删除的数据结构。B树&#xff0c;概括来说是一个节点可以拥有多于2个子节点的二…

数学建模与数据分析 || 2. 结构化与非结构化数据的读取方法

结构化与非结构化数据的读取方法 文章目录结构化与非结构化数据的读取方法1. 结构化数据的读取1.1 pandas 读取 excel 文件1.2 pandas 读取 csv 文件1.3 pandas 读取 txt 文件1.4 利用 scipy 读取 mat 格式文件数据1.5 利用 numpy 存储和读取 npz 格式文件2. python 读取图像的…

SpringBoot国际化

软件的国际化软件的国际化&#xff1a;软件开发时&#xff0c;要使它能同时应对世界不同地区和国家的访问&#xff0c;并针对不同地区和国家的访问&#xff0c;提供相应的、符合来访者阅读习惯的页面或数据。国际化internationalization&#xff0c;在i和n之间有 18 个字母&…

AXI 总线协议学习笔记(2)

引言 从本文开始&#xff0c;正式系统性学学习AXI总线。 如何获取官方协议标准&#xff1f; 第一步&#xff1a;登陆官网&#xff1a;armDeveloper 第二步&#xff1a;登录&#xff0c;无账号需要注册 第三步&#xff1a;点击文档 第四步&#xff1a; 第五步&#xff1a;浏…

最新最全阿里内推830道面试题合集,BATJ都有问到

小小叹语&#xff1a;你是否对你现在的生活状态有满足感呢&#xff1f;逝去日子经过多少风雨波折才有今天的成就&#xff0c;只有努力向上不断闯断&#xff0c;热爱竟逐每秒每分钟&#xff0c;才能拥有的更多。 而对于一个程序员来说&#xff0c;如果说你是想要在互联网行业找…

【题解】2023牛客寒假算法基础集训营4

目录A 清楚姐姐学信息论思路B. 清楚姐姐学构造思路C. 清楚姐姐学01背包(Easy Version)思路D. 清楚姐姐学01背包(Hard Version)思路E. 清楚姐姐打怪升级思路F. 清楚姐姐学树状数组思路G. 清楚姐姐逛街(Easy Version)思路L. 清楚姐姐的三角形I思路M. 清楚姐姐的三角形II思路A 清楚…

Grafana 系列文章(四):Grafana Explore

&#x1f449;️URL: https://grafana.com/docs/grafana/latest/explore/ &#x1f4dd;Description: Explore Grafana 的仪表盘 UI 是关于构建可视化的仪表盘。Explore 剥离了仪表盘和面板选项&#xff0c;这样你就可以。.. Grafana 的仪表盘 UI 是关于构建可视化的仪表盘的。…

happen-before

happen-before 什么是happen-before JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系&#xff0c;尽管a操作和b操作在不同的线程中执行&#xff0c;但JMM向程序员保证a操作将对b操作可见). …

Leetcode(上)

Leetcode&#xff08;上&#xff09; 1.LeetCode01 两数之和 给定一个整数数组nums和一个整数目标值target&#xff0c;请你在该数组中找出和为目标值target的那两个整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一…

数据结构——优先级队列和堆

目录 一、堆 1.概念 2.堆的存储方式 3.性质 4.模拟实现堆&#xff08;以小根堆为例&#xff09; &#xff08;1&#xff09;.堆的调整 &#xff08;2&#xff09;.堆的创建 &#xff08;3&#xff09;.建堆的时间复杂度 &#xff08;4&#xff09;.堆的插入和删除 5.堆…

微服务篇之Eureka注册中心

目录 1. 初识Eureka 1.1 Eureka是什么 1.2 什么是注册中心 1.3 Eureka的原理 2. Eureka的快速入门 2.1 搭建eureka的单机服务 2.2 注册服务的消费者 2.3 注册服务的提供者 3. Eureka的特性 3.1 自我保护机制 3.2 集群支持AP特性 4. Eureka的集群 4.1 不分区集群模式 4.2 分…

Go语言测试(回归测试、集成测试、单元测试简述)与项目开发的流程简述

测试项目流程1. 测试的类别2. 单元测试的规则&#xff08;函数以Test开头&#xff09;2.1 示例12.2 示例23. Mock测试&#xff08;打桩&#xff09;4. 基准测试&#xff08;类似于单元测试&#xff0c;函数以Benchmark开头&#xff09;5. 项目开发的流程项目拆解代码设计测试运…

浪涌保护器(电涌保护器)连接线规格分析方案

低压配电设计中&#xff0c;现在对于浪涌保护器(SPD)及其专用保护装置的标注和画法&#xff0c;都比较规范统一了。那有没有遇到要求标注浪涌保护器连接线规格的情况&#xff1f;或者说&#xff0c;设计师有没有责任要标注清楚各类浪涌保护器连接线规格&#xff1f;地凯科技防雷…

屈光发育档案是什么?为什么专业医生建议从3岁开始就要建立?

当孩子出现近视问题时&#xff0c;家长们都会很焦虑。其实儿童视力发育是一个循序渐进&#xff0c;逐渐成长完善的过程。我们唯一能做的就是预防&#xff0c;在未近视时提前发现近视的趋势。来源&#xff1a;卫生健康委网站这其中最为关键的是建立屈光发育档案。国家青少年近视…

视频剪辑有这6个高清视频素材库就够了

视频剪辑必备的6个网站&#xff0c;免费、可商用&#xff0c;建议收藏&#xff01; 1、菜鸟图库 https://www.sucai999.com/video.html?vNTYxMjky 菜鸟图库网素材类型非常多&#xff0c;平面设计、UI设计、电商类、图片、视频、音频等素材站内都能找到。视频素材全部高清、无…