LeetCode-001:Python 实现哈希表求两数之和:写给学过 Python 但又有点忘了的你

给学过 Python 程序设计,但现在有点忘了的同学,尽量讲得慢一点、顺一点,不装懂,也不跳步。


一、先说这道题在问什么

"两数之和"是 LeetCode 里非常经典的一道入门题。

题目大意是:

给你一个整数数组 nums 和一个目标值 target,请你在数组中找到 两个数 ,让它们相加等于 target,并返回这两个数的下标

比如:

python 复制代码
nums = [2, 7, 11, 15]
target = 9

因为 2 + 7 = 9,所以答案就是:

python 复制代码
[0, 1]

注意,这里返回的不是两个数本身,而是它们在数组中的位置,也就是下标。


二、最容易想到的方法:暴力枚举

如果我们刚学完 Python,最先想到的通常是:

把数组里的每两个数都试一遍,看看有没有加起来等于 target 的。

代码可以这样写:

python 复制代码
def twoSum(nums, target):
    n = len(nums)
    for i in range(n):
        for j in range(i + 1, n):
            if nums[i] + nums[j] == target:
                return [i, j]
    return []

这段代码不难理解:

  • 外层循环先固定第一个数
  • 内层循环再去找第二个数
  • 如果两个数相加等于目标值,就返回它们的下标

举个例子

python 复制代码
nums = [2, 7, 11, 15]
target = 9

程序会这样找:

  • 2 + 7 = 9,找到了
  • 返回 [0, 1]

这个方法是对的,但有个问题:

因为它会把很多组合都试一遍。

如果数组很长,这种做法效率就不高了。


三、为什么它慢

假设数组里有 n 个元素。

暴力方法中:

  • 第一个数要遍历很多次
  • 对于每个第一个数,第二个数也要继续遍历

所以总的时间复杂度是:

python 复制代码
O(n²)

你可以简单理解为:

数据一多,程序要试很多很多对数字。

那有没有一种方法,让我们不用每次都重新去数组里找另一个数?

有,这就要用到 哈希表


四、什么是哈希表

很多初学者一看到"哈希表"这三个字就紧张,觉得这一定是很高级的数据结构。

其实在 Python 里,你早就接触过它了。

Python 里的哈希表,就是 dict

比如:

python 复制代码
d = {}
d[2] = 0
d[7] = 1

这个 d 本质上就是一个哈希表。

它存的是"键 -> 值"的对应关系:

  • 2 对应值 0
  • 7 对应值 1

在这道题里,我们可以让:

  • :数组中的数字
  • :这个数字对应的下标

也就是说,我们一边遍历数组,一边把"某个数出现过,而且它在什么位置"记下来。

这样以后想找某个数是否出现过,就不用重新遍历整个数组了,直接去字典里查就行。

这就是哈希表的核心作用:查找快


五、这道题为什么适合用哈希表

题目要我们找两个数:

python 复制代码
x + y = target

如果当前我们正在看一个数 x,那我们其实只需要知道:

python 复制代码
target - x

有没有在前面出现过。

例如:

python 复制代码
target = 9
当前数 x = 7

那我们就要找:

python 复制代码
9 - 7 = 2

只要之前见过 2,就说明答案找到了。

所以问题就变成:

如何快速判断一个数之前有没有出现过?

这正是哈希表擅长的事。


六、核心思路:边查边存

我们可以一边遍历数组,一边做两件事:

  1. 先查:target - 当前数 在不在哈希表里
  2. 再存:把当前数和它的下标存到哈希表里

代码如下:

python 复制代码
def twoSum(nums, target):
    hashtable = {}
    for i, x in enumerate(nums):
        if target - x in hashtable:
            return [hashtable[target - x], i]
        hashtable[x] = i
    return []

这就是这道题最经典的写法。


七、先别急,这段代码一行一行看


1. 创建哈希表

python 复制代码
hashtable = {}

这里创建了一个空字典。

也可以写成:

python 复制代码
hashtable = dict()

两种写法都行。

这个字典后面会用来记录:

  • 某个数字出现过没有
  • 如果出现过,它的下标是多少

2. 遍历数组

python 复制代码
for i, x in enumerate(nums):

这句很多人一开始不熟,我当时也容易卡住。

它的意思是:

遍历数组 nums,并且同时拿到下标和元素值

比如:

python 复制代码
nums = [2, 7, 11, 15]

那么:

python 复制代码
for i, x in enumerate(nums):
    print(i, x)

输出就是:

python 复制代码
0 2
1 7
2 11
3 15

也就是说:

  • i 是下标
  • x 是当前元素

你可以把它理解成:

python 复制代码
for 下标, 元素 in 枚举(nums):

如果你对这个写法还不熟,它其实等价于下面这种更"传统"的写法:

python 复制代码
for i in range(len(nums)):
    x = nums[i]

只是 enumerate(nums) 更方便、更常用。


3. 先查找配对数

python 复制代码
if target - x in hashtable:

这句非常关键。

假设当前的数是 x,那我们要找的另一个数就是:

python 复制代码
target - x

如果这个数已经在哈希表里,说明我们之前已经见过它了。

那当前这个 x 和之前那个数,正好能凑成 target


4. 找到就返回答案

python 复制代码
return [hashtable[target - x], i]

这里返回的是两个下标:

  • hashtable[target - x]:之前那个配对数的下标
  • i:当前数的下标

例如当前 x = 7,而 target = 9,那我们找的是 2

如果哈希表里有:

python 复制代码
{2: 0}

那说明数字 2 在下标 0

当前 7 在下标 1

所以返回:

python 复制代码
[0, 1]

5. 没找到就把当前数存进去

python 复制代码
hashtable[x] = i

意思是把"当前数字 -> 当前下标"这组信息存起来。

例如:

python 复制代码
x = 2
i = 0

那就相当于:

python 复制代码
hashtable[2] = 0

表示数字 2 出现在下标 0


八、为什么一定要"先查再存"

这个顺序不能乱。

正确写法是:

python 复制代码
if target - x in hashtable:
    return [hashtable[target - x], i]
hashtable[x] = i

为什么不能先存再查?

因为题目要求是找两个不同位置 的数。

如果你先把当前数存进去,再去查,就可能把自己和自己配对了。

举个例子:

python 复制代码
nums = [3, 2, 4]
target = 6

当你遍历到第一个 3 时:

  • 如果先存,哈希表里就有了 3
  • 再查 target - 3 = 3
  • 就会误以为找到了配对

但实际上,这个 3 只有一个,不能和自己配。

所以一定要:

  • 先看前面有没有我要找的数
  • 再把自己存进去

这样才能保证匹配到的是之前的元素,而不是自己。


九、完整模拟一遍执行过程

我们用这个例子来走一遍:

python 复制代码
nums = [2, 7, 11, 15]
target = 9

初始状态:

python 复制代码
hashtable = {}

第 1 轮循环

当前:

python 复制代码
i = 0
x = 2

先查:

python 复制代码
target - x = 9 - 2 = 7

哈希表里有没有 7

没有,因为现在哈希表还是空的。

那就把当前数存进去:

python 复制代码
hashtable[2] = 0

此时哈希表变成:

python 复制代码
{2: 0}

第 2 轮循环

当前:

python 复制代码
i = 1
x = 7

先查:

python 复制代码
target - x = 9 - 7 = 2

哈希表里有没有 2

有,而且它对应的下标是 0

说明之前见过数字 2,现在又遇到了 7,它们正好相加等于 9

于是返回:

python 复制代码
[0, 1]

程序结束。


十、这段代码到底快在哪

暴力方法里,我们要反复去数组里找另一个数,查找很慢。

哈希表方法里,我们把已经出现过的数字存在字典里。

以后只需要一句:

python 复制代码
if target - x in hashtable:

就能快速判断它在不在。

因此,整个过程只需要遍历一遍数组。

时间复杂度

python 复制代码
O(n)

意思是:数组有多少个元素,就大致操作多少次。

空间复杂度

python 复制代码
O(n)

因为我们额外开了一个哈希表,最坏情况下可能要把所有元素都存进去。


十一、适合初学者记住的模板

这题你不用死记整段代码,只要记住下面这个模板就行:

python 复制代码
def twoSum(nums, target):
    d = {}
    for i, x in enumerate(nums):
        if target - x in d:
            return [d[target - x], i]
        d[x] = i
    return []

你可以把它翻译成人话:

  • d = {}:准备一个字典,记录数字和它出现的位置
  • for i, x in enumerate(nums):遍历数组,同时拿到下标和值
  • if target - x in d:看我要找的配对数字之前有没有出现过
  • return [d[target - x], i]:如果出现过,直接返回两个下标
  • d[x] = i:如果没出现,就把当前数字存进去

十二、你现在最容易卡住的几个点

1. "哈希表"听起来陌生,其实就是 dict

在 Python 里,做这题时:

  • 哈希表 = 字典 dict

所以别把"哈希表"想得太吓人。

你平时写的:

python 复制代码
d = {}
d[5] = 10

本质上就是在用哈希表。


2. enumerate(nums) 不用怕

它就是"遍历列表时,同时拿到下标和值"。

python 复制代码
for i, x in enumerate(nums):

等价理解为:

python 复制代码
for 下标, 元素 in nums:

当然严格来说不是这么写,但你脑子里可以这么翻译。


3. in hashtable 检查的是"键"在不在

比如:

python 复制代码
d = {2: 0, 7: 1}
print(2 in d)   # True
print(0 in d)   # False

这里检查的是键,不是值。

所以:

python 复制代码
if target - x in hashtable:

检查的是"这个数字有没有作为键存进去"。


十三、完整可运行代码

最后放一份完整版本:

python 复制代码
def twoSum(nums, target):
    hashtable = {}
    for i, x in enumerate(nums):
        if target - x in hashtable:
            return [hashtable[target - x], i]
        hashtable[x] = i
    return []


nums = [2, 7, 11, 15]
target = 9
print(twoSum(nums, target))

输出结果:

python 复制代码
[0, 1]

十四、写给现在的你

如果你是"大一学过 Python 程序设计,但现在有点忘了"的状态,其实很正常。

很多人不是不会,而是:

  • 语法有点生
  • 代码一长就容易慌
  • 听到"哈希表""时间复杂度"就觉得自己不行

但这题真正核心的东西没有那么多:

  1. 知道字典 dict 可以快速查找
  2. 知道 enumerate 是同时拿下标和值
  3. 知道这题要找的是 target - 当前数
  4. 知道顺序要先查再存

把这四点想明白,这题就通了。

你现在不需要追求"一看就会自己秒写",

只需要做到:

  • 能读懂代码
  • 能跟着例子模拟执行
  • 过一会儿自己再默写一遍

这就已经是在进步了。


十五、小结

"两数之和"这道题的关键,不是在于代码有多复杂,而是在于你有没有把问题转化成:

当前数的配对值有没有在前面出现过?

而 Python 里的字典 dict,正好可以帮我们快速完成这件事。

所以最终我们用哈希表,把原本较慢的双重循环,优化成了只遍历一次数组的写法。

最经典的实现就是:

python 复制代码
def twoSum(nums, target):
    d = {}
    for i, x in enumerate(nums):
        if target - x in d:
            return [d[target - x], i]
        d[x] = i
    return []

这不仅是一道题的解法,其实也是很多算法题里很常见的思路:
一边遍历,一边记录,一边查找。


相关推荐
孤飞1 年前
计算机体系结构 胡伟武 课后习题期末题库重点选集解析Ⅱ(4-7章)
复习指导
孤飞2 年前
推免复习(一):数据库复习提纲
复习指导