三数之和为0

news2024/11/18 11:44:37

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 。

2. 解题思路

求三数之和的这道编程题如果直接用暴力破解,使用三重for循环,那么时间复杂度T(N)=O(N^3),效果很差,可以考虑先将数组排序(排序之后去重会比较方便),然后使用高低位指针向中间逼近的方式找三数相加和为0的值。

2.1 具体步骤

  1. 使用快排将数组排序。
  2. 取三个值,nums[i]、nums[j]、nums[k],初始状态为i=0,j=i+1,k=len-1(len为数组的长度)。
  3. 索引 i 逐渐向高位移动,对于每一次确定的nums[i]值,在 k > j 的情况下 ,若 nums[i] + nums[j] + nums[k] <= 0,则nums[j]从 i+1 的索引位置开始向高位移动, 若 nums[i] + nums[j] + nums[k] > 0,则nums[k]从索引 len-1 的位置向低位移动, 即 j 和 k 只会有一个移动,直至不满足 k > j 的条件则停止移动,结束此次遍历。
  4. 重复步骤三,直至数据遍历完成。

注意:在取i、j、k索引位置的值的时候,要注意去重的细节处理。

2.2 图形展示

在这里插入图片描述

3. 代码实现

    public List<List<Integer>> threeSum(int[] nums) {
        int len = nums.length;
        List<List<Integer>> allList = new ArrayList<>();
        if (len < 3) {
            return allList;
        }
        Arrays.sort(nums);
        for (int i = 0; i < (len - 2); i++) {
            int k = len - 1;
            // 排序之后,最小的索引的数据大于0,就不会出现 nums[i] + nums[j] + nums[k] == 0 的场景,直接跳出循环即可
            if (nums[i] > 0) {
                break;
            }
            // 排序之后,最大的索引的数据小于0,就不会出现 nums[i] + nums[j] + nums[k] == 0 的场景,直接跳出循环即可
            if (nums[k] < 0) {
                break;
            }

            if (i > 0 && nums[i] == nums[i-1]) { // 相同的元素,只能取一个
                continue;
            }

            for (int j = i + 1; j < (len - 1); j++) {
                if (j > i + 1 && nums[j] == nums[j-1]) { // 相同的元素,只能取一个
                    continue;
                }
                // while循环不能在if(k<=j)判断下面,否则会有问题,比如数组 int[] nums = {-6,1,3,5};
                while (k > j && (nums[i] + nums[j] + nums[k] > 0)) {
                    k--;
                }
                if (k <= j) {
                    break;
                }

                if ((nums[i] + nums[j] + nums[k]) == 0) {
                    List<Integer> tempList = Arrays.asList(nums[i], nums[j], nums[k]);
                    allList.add(tempList);
                }
            }

        }
        return allList;
    }

4. 代码分析

for循环的代码,是将相同的代码重复多次,所以需要处理好边界问题,对于三数之和的问题,除了处理好边界问题,还要考虑去重的处理。

4.1.1 索引 i 的边界问题处理

对应代码:

for (int i = 0; i < (len - 2); i++) {//..}

可以看到 i 从0开始, i < len - 2(len是数组的长度),即 i 可以取的最大值为len - 3,结合图说明
索引图片
因为需要取三个元素,所以索引 i 取到len-3即可。

4.1.2 索引 j 的边界问题处理

对应代码:

for (int j = i + 1; j < (len - 1); j++) {//..}

可以看到 j 从 i + 1 开始,j < len - 1(len是数组的长度),即 j 可以取的最大值为len - 2,结合图说明(图同索引 i 对应的图)

4.1.3 索引 k 的边界问题处理

对应代码:

for (int i = 0; i < (len - 2); i++) {
    // 省略无关代码
	// ...
	int k = len - 1;
	// 省略无关代码
	// ...
	for (int j = i + 1; j < (len - 1); j++) {
		// 省略无关代码
		// ...
		while (k > j && (nums[i] + nums[j] + nums[k] > 0)) {
		    k--;
		}
		if (k <= j) {
		    break;
		}
	    // 省略无关代码
	    // ...
	}
}
// 省略无关代码
// ...

可以看到对于每一次循环,k 都是从 len-1 的位置开始,若 nums[i] + nums[j] + nums[k] > 0, 则 k 向低位索引移动,直至不再满足 k > j。即k的边界是动态的。结合图说明(图同索引 i 对应的图)

4.2.1 索引 i 取值的去重处理

对应代码:

// 省略无关代码
// ...
for (int i = 0; i < (len - 2); i++) {
	// 省略无关代码
	// ...
	
	//索引 i 取值的去重代码
    if (i > 0 && nums[i] == nums[i-1]) { // 相同的元素,只能取一个,即相同的值,取索引最小的位置
         continue;
    }

}
// 省略无关代码
// ...

去重的代码中if的判断条件满足的情况下,当前索引 i 对应数组中的值不会被取到,i > 0 保证了nums[i-1] 不会发生索引越界的问题,并且可以取到 i = 0 位置的值(这也是一个边界的处理),nums[i] == nums[i-1] 进行 continue保证了相同元素不断跳过,直至 nums[i] != nums[i-1],这个时候 i 位置的值是可以取的,下面结合一个具体的数组进行说明。见下图:
在这里插入图片描述
上图只对i循环分析了6次,可以看到,排序之后相同值会紧邻,相同的值取值的时候取的都是索引最小位置。

4.2.2 索引 j 取值的去重处理

对应代码:

// 省略无关代码
//...
 for (int j = i + 1; j < (len - 1); j++) {

      // 相同的元素,只能取一个, 这里要说明的是,当j从重复元素取值时,若这个值和i取的相同,则j取值的索引仅大于i取值的
      // 索引,若不同,则j取值取的是相同值的最小索引位置的值
     if (j > i + 1 && nums[j] == nums[j-1]) {
         continue;
     }
     // 省略无关代码
     //...
 }
 // 省略无关代码
//...

j取值从 i+1 的位置开始取值,j > i + 1 的时候判断 nums[j] == nums[j-1] 是否成立,如果成立则跳过,保证了相同的值 j 索引只取一次。下面结合一个具体的数组进行说明。见下图:
在这里插入图片描述
可以看到,在上图的示例中,我们给了索引 i 为0的场景下,索引 j 可以取到的值,当 i 和 j 取的是相同的值-6时,i取的是重复的值-6中最小索引位置的元素,j 取值-6的索引仅大于 i 取值-6的索引,且 j 对于 -6 只取一次,当取值-5时,j 由于和 i 取的不是相同的值,所以j取值取到了-5最小索引位置的值,且只取了一次。保证了重复数据的去重问题。

4.2.3 索引 k 取值的去重处理

对应代码:

for (int i = 0; i < (len - 2); i++) {
    // 省略无关代码
	// ...
	int k = len - 1;
	// 省略无关代码
	// ...
	for (int j = i + 1; j < (len - 1); j++) {
		// 省略无关代码
		// ...
		while (k > j && (nums[i] + nums[j] + nums[k] > 0)) {
		    k--;
		}
		if (k <= j) {
		    break;
		}
        if ((nums[i] + nums[j] + nums[k]) == 0) {
            List<Integer> tempList = Arrays.asList(nums[i], nums[j], nums[k]);
            allList.add(tempList);
        }
	}
}
// 省略无关代码
// ...

可以看到,在 k > j 的前提下,若 nums[i] + nums[j] + nums[k] > 0, 则k会一直向低位索引移动,假设k移动到某个位置 k2(紧挨着k2左侧的k1和k2的值相同),使得 nums[i1] + nums[j1] + nums[k2] = 0,这时会跳出while循环,将这组满足相加为0的集合放到allList,然后执行 j++ ,即 j 右移的操作,所以取不到nums[i1] + nums[j1] + nums[k1] = 0 这个和 nums[i1] + nums[j1] + nums[k2] = 0 重复的组合。下面结合一个具体的数组进行说明。见下图:
在这里插入图片描述
从上图的5个步骤可以看到,在k > j 前提下,k向左移动,nums[0] + nums[1] + nums[4] >0不成立,k停止移动,将nums[0] + nums[1] + nums[4]=0的组合放到集合中,然后j开始右移。所以尽管nums[3]=nums[4],但是我们取不到nums[0] + nums[1] + nums[3]=0这个组合,保证了k不会取重复的值。

4.3 代码的细节补充说明

代码如下:

for (int i = 0; i < (len - 2); i++) {
    // 省略无关代码
	// ...
	int k = len - 1;
	// 省略无关代码
	// ...
	for (int j = i + 1; j < (len - 1); j++) {
		// 省略无关代码
		// ...
		// while循环不能在if(k<=j)判断下面,否则会有问题,比如数组 int[] nums = {-6,1,3,5};
		while (k > j && (nums[i] + nums[j] + nums[k] > 0)) {
		    k--;
		}
		if (k <= j) {
		    break;
		}
	}
}

while循环的代码不能和if(k<=j) 这个判断互换位置,互换位置会有问题,下面结合一个具体的数组进行说明。见下图:
在这里插入图片描述
由上图可以看到,k<=j若放在while循环的下方,会出现 j 和 k 取了相同索引非法值的情况。总结一下就是:若数组中 存在 nums[a-1] + nums[a+1] = 2nums[a], 且有nums[i] + nums[a-1] + nums[a+1]=0 (i < a-1),k<=j若放在while循环的下方就会取到非法值。

5. 时间复杂度

T(N) = O(N^2),快排的时间复杂度为T(N) = O(NlogN);由于 j 和 k 只会有一个移动,所以for循环的时间复杂度是
T(N) = O(N^2),
所以整体时间复杂度是 T(N) = O(N^2)

6. 空间复杂度

空间复杂度为 O(logN), 在不考虑存储结果集的情况下。若不能改变原数组,则还需要复杂一份原数组的值,那么空间复杂度为O(N)。

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

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

相关文章

Vscode超好看的渐变主题插件

样式效果&#xff1a; 插件使用方法&#xff1a; 然后重启&#xff0c;之后会显示vccode损坏&#xff0c;不用理会&#xff0c;因为这个插件是更改了应用内部代码&#xff0c;直接不再显示即可。

GUPAO-AI大模型实战训练营-大模型原理及训练技巧、大模型微调的核心原理

在当今科技日新月异的时代&#xff0c;大模型已经成为人工智能领域的重要支柱。GUPAO-AI大模型实战训练营&#xff0c;正是为了深入解析这些庞然大物背后的原理&#xff0c;以及如何有效利用它们进行实际操作和微调。本文将带你走进大模型的殿堂&#xff0c;揭示其原理&#xf…

【重学 MySQL】四十二、单行子查询

【重学 MySQL】四十二、单行子查询 单行子查询的基本用法示例1&#xff1a;查找薪资高于公司平均水平的员工示例2&#xff1a;查找没有分配项目的员工示例3&#xff1a;使用单行子查询进行等值比较 注意事项 在MySQL中&#xff0c;子查询&#xff08;Subquery&#xff09;是一种…

VUE 整合 ECharts

一、vue 引入 ECharts依赖 npm install echarts --save 二、创建盒子 <div ref"chars" style"height: 500px;width:800px;"></div> 解释说明 ref"chars" 是 Vue.js 中一个非常有用的特性&#xff0c;用于给 DOM 元素或组件实例…

CrossOver24支持的游戏有那些

CrossOver刚刚更新了24版本&#xff0c;支持《地平线零之曙光》、《以撒的结合&#xff1a;重生》等游戏。一起来看看它有哪些更新吧&#xff01; 一、功能优化 - 更新 Wine 至最新的稳定版 Wine 9.0&#xff0c;引入了 7000多个更新和针对各种软件游戏的优化。 - 更新 Wine M…

Android平台GB28181实时回传流程和技术实现

规范解读 GB28181 中的 “INVITE” 是会话初始协议&#xff08;SIP&#xff09;中的一种请求方法&#xff0c;主要用于邀请一个或多个参与者加入特定的会话。在 GB28181 标准中&#xff0c;“INVITE” 请求通常用于发起媒体流的传输请求。当一个设备想要接收来自另一个设备的媒…

Linux上安装Jenkins并展示allure报告

1. 确认安装正确的java版本 到官网War Jenkins Packages查看Jenkins版本匹配的java版本&#xff0c;我这里选择安装java11 使用java --version命令是否已安装java版本 java --version 如上图所示&#xff0c;暂未安装java版本&#xff0c;我这里选择安装java11&#xff08;je…

基于SpringBoot+Vue+MySQL的在线酷听音乐系统

系统展示 用户前台界面 管理员后台界面 系统背景 随着互联网技术的飞速发展&#xff0c;网络已成为人们日常生活中不可或缺的一部分。在线音乐服务因其便捷性和丰富性&#xff0c;逐渐成为用户获取音乐内容的主要渠道。然而&#xff0c;传统的音乐播放平台往往存在歌曲资源有限…

进程组、会话、守护进程和线程的概念

1.进程组和会话 1.1 概念和特性 进程组&#xff0c;也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念&#xff0c;是为了简化对多个进…

微信小程序-数据模型与动态赋值

首先新建一个小程序项目. 这边有创建基础项目的流程:从0新建一个微信小程序实现一个简单跳转_小白开发小程序源代码-CSDN博客 一共两步: 1.建立页面的 数据模型 和 默认赋值: 默认赋值: 2.接收输入框的新文案,动态替换上面的文案展示 //文件 testUI.js增加方法:onInputChan…

当 ucx --with-cuda 时做了什么

1&#xff0c;找一只活麻雀&#xff0c;下载编译 ucx git clone https://github.com/openucx/ucx.git cd ucx/ git checkout v1.16.0 ./autogen.sh ./autogen.sh mkdir build cd build ../contrib/configure-devel --with-cuda/usr/local/cuda --without-rocm --without-java …

JavaScript 知识点 - 作用域(变量提升、垃圾回收机制、闭包)

一、作用域 1、基本概念 是什么? 指变量、对象和函数在【代码中的可访问性范围】。 有什么用? 理解作用域对【编写高效和无错误的代码】至关重要 分类 局部作用域&#xff08;函数作用域、块作用域&#xff09;、全局作用域 涉及到那些知识点 作用域链、JS垃圾回收机…

在线支付系统

一、系统概述 本在线支付系统基于 Servlet 技术构建&#xff0c;旨在为用户提供安全、便捷的支付服务。系统具备简洁的用户界面和高效的支付处理能力&#xff0c;满足用户在各种场景下的支付需求。 二、主要功能 首页登录注册&#xff1a; 用户可以在首页进行登录和注册操作。注…

MacBook 使用 brew 安装 MySQL

目录 &#xff08;1&#xff09;准备工作1.1 更新 brew &#xff08;2&#xff09;正式安装2.1 安装MySQL&#xff1a;2.2 启动mysql &#xff08;3&#xff09;初始化数据库3.1 选择验证密码组件3.2 密码强度3.3 删除匿名用户3.4 禁用root用户远程连接3.5 删除test数据库3.6 重…

C语言 17 宏定义

前面认识了#include指令&#xff0c;接着来看#define指令&#xff0c;它可以实现宏定义。宏是啥意思&#xff1f; 把参数批量替换到文本中&#xff0c;这种实现通常称为宏&#xff08;macro&#xff09;或定义宏 (define macro) 可以通过#define来定义宏&#xff0c;规则如下&a…

Cyber Weekly #26

赛博新闻 1、Meta发布最强AR眼镜 Meta Connect 2024大会展示了多款新产品和技术&#xff0c;包括更便宜的Quest 3S系列AR眼镜、新功能丰富的Meta Rayban眼镜、OrionAR眼镜原型机&#xff0c;以及月活5亿用户的Meta AI。其中&#xff0c;OrionAR眼镜以其先进的交互体验和强大的…

鸿蒙开发(NEXT/API 12)【硬件(传感器开发3)】传感器服务

场景介绍 当设备需要获取传感器数据时&#xff0c;可以使用sensor模块&#xff0c;例如&#xff1a;通过订阅方向传感器数据感知用户设备当前的朝向&#xff0c;通过订阅计步传感器数据统计用户的步数等。 函数说明 名称描述OH_Sensor_GetInfos(Sensor_Info **infos, uint32…

算力运力解决方案:构建未来智算新生态

中国联通国际有限公司产品之算力运力解决方案&#xff1a;构建未来智算新生态 在当今这个数据爆炸、技术日新月异的时代&#xff0c;算力已成为推动社会进步和产业升级的关键力量。中国联通国际有限公司紧跟时代步伐&#xff0c;依托其强大的网络资源和深厚的技术积累&#xf…

Linux进程间的通信(四)System-V共享内存

什么是共享内存 共享内存&#xff0c;顾名思义就是允许两个不相关的进程访问同一个逻辑内存&#xff0c;共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。 不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空…

深度学习后门攻击分析与实现(二)

前言 在本系列的第一部分中&#xff0c;我们已经掌握了深度学习中的后门攻击的特点以及基础的攻击方式&#xff0c;现在我们在第二部分中首先来学习深度学习后门攻击在传统网络空间安全中的应用。然后再来分析与实现一些颇具特点的深度学习后门攻击方式。 深度学习与网络空间…