踩了二维数组的坑。。

科技   2022-03-23 09:45  

大家好,我是「负雪明烛」。

这是「刷题躲坑」系列的第一篇文章,这个系列帮助大家躲掉刷题/代码中常见的坑。

怪事

在 Python 中,定义长度为 3,数值全为 0 的一维列表(也就是数组)的方式有:

a = [000
a = [0 for _ in range(3)] 
a = [0] * 3

在刷算法题时,我们常常遇到的一个场景,就是需要定义一个和输入等长的一维列表。所以,我基本都是用上述的第 3 种方式。

如:

a = [0] * len(nums)

假设 nums 的长度是 5,那么运行完上述代码以后,a 的值为 [0, 0, 0, 0, 0]

我们修改一下 a 列表:

In [1]: a[0] = 666

In [2]: a
Out[2]: [6660000]

可以看到,修改 a 列表的第 0 个元素并不会影响到其他位置的元素。

这是理所当然的事情。

可是,当数组变成二维的时候,怪事就发生了

按照上述定义一维数组的思路,定义一个 32 列的二维数组 b

In [1]: b = [[0] * 2] * 3

In [2]: b
Out[2]: [[00], [00], [00]]

看起来 b 是符合要求的,没问题吧。

但是,当我们修改 b 中的元素时:

In [1]: b[0][0] = 2333

In [2]: b
Out[2]: [[23330], [23330], [23330]]

看到了吗?我们只修改了 b[0][0],但是 b 列表中的第 0 列的所有元素,全部都被修改了!!

是不是很奇怪??

分析

遇到这种奇怪的问题时,可以用我之前分享的 代码执行过程可视化,拯救了我的脑细胞!进行分析。

这个工具的地址是:https://pythontutor.com/

先看 Python 一维列表,在内存中的可视化运行情况。



从上面的动图可以看到,修改一维数组中的某个元素,并不会影响到其他元素。

再看按照之前方式定义的 Python  二维列表,在内存中的运行情况。

我们本以为的正确的二维列表应该是下面这样。即数组有 3 行,每行都是一个独立的长度为 2 的列表。



可实际上呢?

当我们定义 b = [[0] * 2] * 3 时,虽然看起来结果是一个 3 x 2 的二维列表,但是在内存中,数组中每行都是指向同一个长度为 2 的列表



因此,当我们修改 b[0][0] 时,虽然只是修改了内存中的一个元素,但是由于 3 行指向的是同一个地址,因此看起来是把 3 行中的元素都修改了。



这就是怪事发生的原因。

我们在 Python 终端中看一下 b3 行的内存地址(可以使用id()函数获取内存地址),进行验证。

In [1]: b = [[0] * 2] * 3
In [2]: for i in range(3):
    ...:     print(id(b[i]))
    ...:
140178338622720
140178338622720
140178338622720

看到 b 列表的 3 行内存地址确实一样的,和我们上面的可视化结果一致。

正确写法

上面是 Python 中常见的坑,负雪在刷题的时候,也被坑过。。

我们可以这么理解:

  • [0] * 2 返回的是一个内存中的地址。

  • 定义 [[0] * 2] * 3时 , [0] * 2  只运行了一次,返回了一个地址x;二维列表中存放了  3x,即[x, x, x]

那 Python 二维数组的正确写法是什么呢?

第一种写法,你可以把所有的元素都显式的写出来,这样肯定没问题:

a = [[00], [00], [00]]

第二种写法,我们使用 for 来定义第二个维度。是推荐的写法。

In [1]: a = [[0] * 2 for _ in range(3)]

In [2]: a
Out[2]: [[00], [00], [00]]

In [3]: a[0][0] = 666

In [4]: a
Out[4]: [[6660], [00], [00]]

为什么使用 for 的写法可以呢?

因为这种情况下 [0] * 2 运行了 3 次,所以返回的是 3 个不同的地址x,y,z。二维列表中的存放的是 [x, y, z]

可视化结果如下:



可以看到 3 行指向不同的地址,这样的结果就是符合预期的了。

总结

今天分享了 Python 刷题中常见的一个坑:二维列表的定义

我们通过代码运行可视化工具,分析了为什么使用 * 定义二维数组会出问题。

最后也给出了使用 for 来定义二维列表的正确写法。

了解内存知识,在编程中非常有用。

推荐新手在遇到这种问题时,使用可视化工具进行排查,非常有助于理解。

以上就是「刷题躲坑」系列的第一篇文章啦!

欢迎关注我的公众号「负雪明烛」,在编程学习路上,我们一起踩坑、躲坑。


这是我持续坚持写作的第 5/7 天。


负雪明烛」,来自于“苍山负雪,明烛天南”,乐于分享与帮助别人。
我坚持刷算法题 7 年,写了 1000 多篇题解,博客累计阅读量 400 万+。
关注我,你可以获得优质算法题解、找工作经验技巧、模拟面试、大厂内推。
这是一个用心在做的公众号,欢迎点击关注+星标!

历史文章推荐:
  1. 面试最常考的 100 道算法题分类整理!
  2. LeetCode 最经典的 100 道题
  3. 直播分享:LeetCode 应该怎么刷?
  4. 我的爆款算法题解是怎么创作出来的?
  5. 写了 1000 篇算法题解是什么体验?
  6. 为什么「执行代码」正确,「提交」出错?


负雪明烛
1000 篇算法题解的作者,国内互联网大厂程序员,技术分享爱好者。 爱好算法题解写作,擅长深入浅出讲解计算机知识,乐于分享大厂见闻。和读者一起刷算法题,拿 Offer,交朋友!