LeetCode题集-1- 两数之和

news/2024/10/5 3:19:09

 

 

这个题目是什么意思呢?简单来说就是在一个数组中找出两个元素,使其和为我们设定的值,并且每个元素只能用一次。

 

如下图具体示例:

 

到这里不知道你是否已经有解题思路了呢?

解法一:双层循环

我第一反应就是双层循环,直接暴力破解。因为题目要求每个元素只能使用一次,并且已经计算过的也没必要再次计算,因此内层循环索引起始可以以外层索引+1作为起始点,具体代码如下:

public static int[] TwoSumForFor(int[] nums, int target)
{for (var i = 0; i < nums.Length; i++){for (var j = i + 1; j < nums.Length; j++){if (nums[i] + nums[j] == target){return [i, j];}}}return [];
}

我们直接验证一下,通过了:

 

因为是双层循环因此算法时间复杂度是:O(N2),因为没有引用额外的空间因此空间复杂度是:O(1)。

:上面的[] ,[i, j]是C#12版本新增功能,是数组简洁表达语法。

解法二:双层循环+左右开弓

如果想在双层循环基础上继续优化算法要怎么办?

我们就按正常思维逻辑来梳理一下双层循环干了什么,外层循环:表示第一个加数,并且从第一个数到最后一个数循环一遍;内层循环:表示第二个加数,其作用就是使第一个加数按从前到后的顺序和其后面的每一个数加一遍。既然如此那能不能从前往后计算的同时也从后往前计算呢?显然使可以的,代码如下:

public static int[] TwoSumForForBidirectional(int[] nums, int target)
{for (var i = 0; i < nums.Length; i++){var front = nums[i];var backIndex = nums.Length - 1 - i;var back = nums[backIndex];for (var j = i + 1; j < nums.Length; j++){if (front + nums[j] == target){return [i, j];}if (back + nums[j - 1] == target){return [j - 1, backIndex];}}}return [];
}

运行结果如下:

 

理想情况下可能会提升一倍的效率,但是细心的朋友应该发现,平台上的运行结果,比双层循环还长了3ms,不过感觉这个平台结果不是很准,下面我们用基准测试,对两个方法进行测试,我们随机构建长度为2000的数组,并把目标数随机放到不同位置,测试10000次。

 

从基准测试的结果来看,整体上并没有提升多少性能。这是因为这个算法本质上时间复杂度还是:O(N2),因此并没有真正起到优化的作用,只有特定的数据分别可能才会有相对较好的表现,这个算法就当作给我们提供了一种解题思路吧。

解法三:单层循环+LastIndexOf

既然左右开弓不行,我们换一个思路,想办法去掉一层循环。

首先外层循环需要保留,因为需要把每个元素都计算一遍,因此我们从内层循环下手。想想题目,是要找到两个数使其和为目标值,那么我们是否可以在循环第一个数时候,通过目标值计算出我们要找的第二个值,看看这个值是否存在,如果存在,则完成算法,否则继续循环直到找到为止。按照这个思路我立马想到C#里的IndexOf和LastIndexOf方法,直接上代码:

  public static int[] TwoSumForLastIndexOf(int[] nums, int target){for (var i = 0; i < nums.Length; i++){var j = Array.LastIndexOf(nums, target - nums[i], nums.Length - 1, nums.Length - 1 - i);if (j >= 0 && j != i){return [i, j];}}return [];}

运行结果如下:

 

:Array.LastIndexOf<T>(T[] array, T value, int startIndex, int count)方法可以指定从什么地方开始查找,查找多少个数。

同样我们再做一次基准测试做对比。

 

可以发现这一版本算法性能大幅提升,但是我们细想一下,LastIndexOf方法本质还是在数组中找一个元素,最坏的情况还是要把整个数组遍历一遍,只能说C#本身做了很好的优化使其性能很高,但是从算法时间复杂度的角度来看,其仍然是 O(N) 的,也就是说这一版本算法时间复杂度还是O(N2)。

解法四:单层循环+字典(哈希)

哪到底如何才能把O(N)的集合查找时间复杂度优化了呢?如果能改造到O(1) 就好了,顺着这个思路还真想到了一种数据结构-哈希表,可以做到O(1) 的查找时间复杂度。

在C#中可以使用Dictionary字典类型,数据结构选好了,下面就是怎么用的问题,key存什么?value存什么?

再次回忆一下题目要求,是找到两个数使其和为目标值,假设x+y=target,x为第一个数,并且外层循环第一个数,那么当处理数据x时,同时能得到y=target-x,如果数组中后面存在值为y的元素,是不是就意味着这对[x,y]就是我们要找的值,因此我们可以在处理x时,把y=target-x和x所在索引记录下来,正好分别对应Dictionary的key和value。代码如下:

  public static int[] TwoSumDictionary(int[] nums, int target){var dic = new Dictionary<int, int>();for (var i = 0; i < nums.Length; i++){if (dic.TryGetValue(nums[i], out var value)){return [value, i];}dic.TryAdd(target - nums[i], i);}return [];}

执行结果如下

 

运行正确,我们再来用基准测试对比一下。

 

结果和我们预想竟然不一样,还没有LastIndexOf方法效果好,哪里出了问题呢?

下面我们对这两种方法单独做一次全方面的基准测试对比,对每个方法分别用数组长度为100,1000,10000三种情况,各进行10000次测试。结果如下:

 

可以发现,只有当数组长度越来越大的时候,哈希表方案优势才慢慢体现出来。

我们再来分析一下这个算法复杂度,首先外层循环时间复杂度为O(N) ,字典操作时间复杂度为O(1),因此整体时间复杂度为O(N) 。因为字典需要额外的存储空间并且最大长度为数组长度减1,因此空间就复杂度为O(N) 。这是经典的以空间换时间方案。

从上面的对比不难发现,即使再好的方案也有其使用场景,数据量的大小,空间的大小都会制约着算法方案的选择,因此需要我们因时制宜选出最合适的方案。

没有最好只有最合适。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

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

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

相关文章

读软件开发安全之道:概念、设计与实施14低级编码缺陷

低级编码缺陷1. 低级编码缺陷 1.1. 在更靠近机器级别的代码中常会出现这类缺陷1.1.1. 越接近硬件级别越能获得最大效率的诱惑仍然很大1.1.2. 更接近硬件级别的编程是非常强大的,但其代价是工作量和脆弱性的增加1.2. 当数据超出了固定的大小,或者超出了分配的内存缓冲区容量时…

八月闲趣之报文

初 无意间得到一段报文,起了好奇心。检索得知,这段报文属于条码打印机的控制指令ZPL,特点为以^或~开头,(此类指令还有ZPL、EPL、CPCL,TSPL,ESC/POS,等等)。 按ZPL手册,逐项对比,指定坐标放置对象,唯~DGR(Download Graphics),涉及一个Z64特性。 举例如下。 ~DGR:LO…

Vue 学习笔记(1):从传统 JavaScript 到 Vue 开发

前言 笔者在学习 Vue 等前端框架前只接触过基本的前端三件套,即 HTML、CSS、JavaScript(原生),在这之前有尝试接触过一些 Vue 教程,了解一些语法,但并不知道各类方法之间到底是什么关系。 近些日子硬着头皮写了几个 Vue 项目,有所心得。好歹是把 MVVM 和工程化之类的概念…

Codeforces Round 969 (Div. 2)题解A-E

Codeforces Round 969 (Div. 2) 神奇的一场,感觉整体不是很难,狠狠的上了一波大分。这场也算是这个暑假的最后一场了整个暑假不是在渡劫就是在渡劫的路上,中间那个紫名还是回滚给加上的,神奇的比赛,每次都能很快打到渡劫的分数,然后不出意料的渡劫失败。不懂 再接再励吧,…

关于Linux内核自带GPIO LED控制

正点原子Linux开发板IMX6ULL上的呼吸灯如何停止? 学习到驱动开发Linux系统自带的LED驱动控制的时候,才知道,原来该呼吸灯经过设备树配置好之后,直接由Linux内核程序配置为呼吸灯(前提是在内核中配置过,可以使用make menuconfig来去配置内核)。 所以在之前写led灯的驱动的…

038.CI4框架CodeIgniter,使用Jwt生成token

01、在composer.json中增加一行调用jwt的代码:{"name": "codeigniter4/appstarter","description": "CodeIgniter4 starter app","license": "MIT","type": "project","homepage"…

OPPO手机备份

通过「数据备份与迁移」备份的资料是存储在手机存储中的,当对手机进行恢复出厂设置或刷机时会清除备份数据,此时,就需要我们在操作前将备份文件拷贝到外置存储或电脑设备中。在「数据备份与迁移」中将资料备份好后,用数据线将手机连接至电脑,根据提示在手机屏幕上选择「传…

财务知识-做账顺序

财务知识-做账顺序