每日语录:你敲下的每一行报错代码,都是未来大神成长路上的通关文牒,别怕看不懂,只怕你不动
对应题目:两数之和
题目难度:简单
相关标签:数组 哈希表
原题复现:
给定一个整数数组nums和一个整数目标值targe,请你在该数组中找出和为目标值target的那两个整数,并返回它们的数组下标
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素
你可以按任意顺序返回答案
示例 1:
输入:
nums = [2,7,11,15], target = 9输出:
[0,1]解释:
因为 nums[0] + nums[1] == 9 ,返回 [0, 1]
示例 2:
输入:
nums = [3,2,4], target = 6输出:
[1,2]
示例3:
输入:
nums = [3,3], target = 6输出:
[0,1]
提示:
-
2 <= nums.length <= 10⁴ -
-10⁹ <= nums[i] <= 10⁹ -
-10⁹ <= target <= 10⁹ -
只会存在一个有效答案
进阶: 你可以想出一个时间复杂度小于 O(n²) 的算法吗
一.审题与界定:别急着写代码,先把题目嚼碎了再说
当你第一次看到这道题时,可能会觉得:“哦,不就是找个两数之和嘛,”但如果你真这么想,很容易踩进一个坑:你以为自己懂了,其实你只懂了表面意思
我们来把题目拆成三个层次:
1.已知条件(我们有什么)
一个整数数组,例如[2, 7, 11, 15]
一个目标值,例如9
2.未知目标(我们要找什么)
找出两个数,它们的和等于目标值9
返回这两个数的下标(注意:不是值本身,是它们在数组里的位置)
3.约束边界(有哪些限制)
每种输入只会有一个答案(这其实是给你的“免死金牌”,意味着你不用担心找到多个结果)
不能使用两次相同的元素(比如[3, 3],你可以用两个3,因为它们是两个不同的元素,虽然值相同,但下标不同)
你可以按任意顺序返回答案([0,1]或[1,0]都算对)
这一步的作用:就像你拿到一份藏宝图,先不急着跑,先把地图上的标记,边界,危险区域都看清楚,很多新手一上来就写代码,结果越写越乱,就是因为没有先把“什么能做,什么不能做”想清楚
二.问题与建模:把生活场景翻译成计算机语言
现在,我们要把这道题从“人类语言”翻译成“计算机能听懂的语言”
生活化类比
想象你在一个班级点名册里,要找出两个学生,他们的年龄加起来恰好等于某个数,你手里有一张名单(数组),上面写着每个人的年龄(值)和座位号(下标)
你可能会怎么做
方法一:从第一个学生开始,依次问后面每一个同学:“你和我加起来等于目标吗,”这就是暴力法
方法二:你每看到一个学生,就拿出一本小本子记下:“这个年龄的学生坐在几号位”,然后你只需要看看“目标年龄减去当前年龄”这个差值,是否已经在小本子里了,这就是哈希法
数学本质
我们要找的是:
nums[i] + nums[j] = target
这个式子可以变形为:
nums[j] = target - nums[i]
也就是说,“求和问题”被转化为了“差值查找问题”
我们不需要再傻傻地两两相加,而是“遍历到一个数,就去问之前有没有存过它的补数”
这一步的作用:把题目从“生活故事”提炼成“数学模型”,这是从“我大概知道怎么做”到“我能用程序逻辑表达”的关键转折
三.策略与推演:为什么“空间换时间”是神来之笔
第一种思路:暴力法(直觉,但慢)
如果你用两个循环遍历所有组合:
先固定第一个数,然后遍历后面所有数,看有没有相加等于target的
时间复杂度是O(n²),当数组长度很大时(比如10⁴个元素),就要计算1亿次组合,电脑会累到“喘气”
第二种思路:哈希法(聪明,又快)
你发现暴力法的本质问题在于:每次查找“补数”都需要重新遍历一遍数组
如果能把之前看过的数存起来,下次直接“查表”,就能瞬间找到
这就是哈希表的威力:
它就像一个“即时贴备忘录”,你每处理一个数,就把它的值和下标记下来
当你看下一个数时,只需要查一下target是否已经在备忘录里,如果在,就直接找到答案了
思考逻辑:
为什么可以用哈希表,因为我们需要“快速查找之前是否出现过某个值”
为什么不用数组或集合,因为数组太大浪费内存,集合只存值不存下标,不符合要求
所以,
map(键值对)是最佳选择——键存数值,值存下标
四.步骤与流程:用自然语言画出“执行路线图”
我们可以把整个算法分为三个清晰的阶段:
阶段一:准备工具
创建一个空的“备忘录”(哈希表),用来存放“已经看过的数字和它的下标”
阶段二:开始遍历
从数组的第一个元素开始,依次取出每个数nums[i]
计算diff = target - nums[i],即“当前数需要匹配的补数”
阶段三:查表与更新
去备忘录里查一下:diff是否已经存在
如果存在:说明我们已经见过一个数,它和当前数正好能凑成target,直接返回那个数和当前的下标
如果不存在:说明当前数暂时没有匹配,但未来可能有别的数需要它,所以把它记进备忘录里,继续遍历
用流程图表示:
开始
创建空哈希表
遍历数组(i从0到结尾)
计算 diff = target - nums[i]
diff 在哈希表中吗?
是 返回 [哈希表[diff], i]
否 把 nums[i] 和 i 加入哈希表
继续下一轮
(如果遍历结束还没返回,说明无解,但题目保证有答案)
注意:这里的关键是“先查再记”
如果你先把当前数记进去,再查补数,万一
diff和当前数一样(比如target=6, nums[i]=3),你可能会查到“自己”,导致错误地使用同一个元素两次,所以必须先查后记
五.边界与验证:提前排雷,让你的代码更健壮
常见的坑点
1.数组长度正好为2:比如nums = [1,2], target=3,这肯定能找到,但你的代码要能处理这种最小规模
2.负数和零:nums和target都可能是负数或零,你的代码应该自然支持,不需要特殊处理(因为只是做加减法)
3.重复元素:比如nums = [3,3], target=6
当你遍历到第一个3时,哈希表还是空的,先记进去
遍历到第二个3时,diff = 3,发现哈希表里有3(下标是0),于是返回[0,1]
这不会出错,因为两个3是不同的元素
4.无解的情况:题目保证有解,所以不必写return nil,但为了代码规范,最好还是有一个默认返回值
测试用例设计
基础用例:nums = [2,7,11,15], target=9 ,应返回[0,1]
乱序用例:nums = [3,2,4], target=6 ,应返回[1,2]
重复元素用例:nums = [3,3], target=6 ,应返回[0,1]
负值用例:nums = [-1,-2,-3], target=-4 ,应返回[0,2]
这一步的作用:让你意识到,题目虽然简单,但“简单”不等于“没坑”,提前想好这些情况,写代码时就不会手忙脚乱
六.最终代码:清晰高效,带丰富注释
// twoSum 是解决 LeetCode 第一题(两数之和)的核心函数
// 参数:nums 为整数数组,target 为目标值
// 返回:一个长度为 2 的整数切片,包含两个数的下标
func twoSum(nums []int, target int) []int {
// 创建一个哈希表(字典),用于存储已遍历过的数值及其对应下标
// 这是典型的“空间换时间”策略
memo := make(map[int]int)
// 遍历数组,i 是当前下标,num 是当前值
for i, num := range nums {
// 计算与当前数配对所需的补数: target - num
diff := target - num
// 去哈希表中查找补数是否已经存在
if j, found := memo[diff]; found {
// 如果存在,说明我们之前已经遇到了一个数与当前数相加等于 target
// 返回那个数的下标 j 和当前数的下标 i
return []int{j, i}
}
// 如果补数不在哈希表中,就把当前数及其下标存入哈希表
// 这样后面的数字在查找时就有可能找到它
memo[num] = i
}
// 根据题目约束,必定存在一个答案,这里仅为了编译完整性而返回空切片
return []int{}
}
七.时间复杂度与空间复杂度分析
时间复杂度:O(n)
理由:我们对数组进行了一次遍历(一个循环),循环次数等于数组长度n,在循环内部,哈希表的查找和插入操作都是O(1)的(平均情况下),因此总的时间复杂度是 O(n)
空间复杂度:O(n)
理由:我们使用了一个哈希表memo,它最多会存储n个键值对(当数组中没有匹配时,所有元素都会被存进去),因此额外的空间开销与数组大小成正比,即O(n)
为什么这是最优的
由于题目要求至少遍历一次数组,所以时间复杂度不可能低于O(n),同时,为了能够在O(1)时间内查找补数,必须使用额外的存储结构(哈希表),因此空间复杂度O(n)是合理的,这已经是在时间与空间之间达到了最佳平衡
八.总结:你迈出的第一步,已经超越了一半人
现在,让我们回过头来看看我们走过的路:
从审题开始,我们没有急着写代码,而是先像侦探一样,把题目里的每一个线索都捋清楚
然后建模,把“找两个数”这个生活场景,翻译成了计算机能理解的“查找补数”问题
接着推演策略,我们对比了“暴力穷举”和“哈希备忘”两种思路,明白了为什么“空间换时间”是一种优雅的取舍
再设计流程,我们画出了清晰的执行步骤,确保每一步都有条不紊
最后验证边界,我们提前发现了那些可能让你掉进去的坑,并一一填平
最终,我们写出了这样一段简洁,高效,带丰富注释的代码,它虽然只有十几行,但背后承载的是“先查后记”,“边遍历边记录”这种在算法世界中反复出现的核心思想
这道题教会我们的三件事:
1.不要害怕暴力——暴力是思维的起点,但绝不是终点,有了暴力,你才能找到优化的方向
2.哈希表是“查缺补漏”的神器——当你需要“快速查找之前是否见过某个元素”时,第一时间想到它
3.先思考,再动手——真正的高手,花在思考上的时间比敲代码的时间多得多
你今天的坚持,就是明天所有难题的“补数”
当你遇到更复杂的问题时,不妨问问自己:“我能不能像两数之和那样,用一个备忘录来简化它”
现在,你已经不是那个对着第一题发愁的萌新了,你学会了分析问题,选择策略,推导步骤,验证边界,这,就是刷题最宝贵的收获
带着这份收获,勇敢地去挑战下一道题吧
版权声明:本文题解及全部内容(含思路,代码,注释,测试用例)的完整知识产权归作者所有,仅供个人学习参考,未经明确书面授权,严禁任何形式的复制,转载,下载,改编或用于商业用途,文中所含测试用例仅用于辅助理解算法逻辑,请勿直接复制提交至在线评测平台,否则可能因不符合题目隐藏数据或格式要求而导致错误,相关后果由使用者自行承担,尊重原创,从每一次独立思考开始