大家好,我是「负雪明烛」。
这是「刷题躲坑」系列的第一篇文章,这个系列帮助大家躲掉刷题/代码中常见的坑。
怪事
在 Python 中,定义长度为 3
,数值全为 0
的一维列表(也就是数组)的方式有:
a = [0, 0, 0]
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]: [666, 0, 0, 0, 0]
可以看到,修改 a
列表的第 0
个元素并不会影响到其他位置的元素。
这是理所当然的事情。
可是,当数组变成二维的时候,怪事就发生了。
按照上述定义一维数组的思路,定义一个 3
行 2
列的二维数组 b
:
In [1]: b = [[0] * 2] * 3
In [2]: b
Out[2]: [[0, 0], [0, 0], [0, 0]]
看起来 b
是符合要求的,没问题吧。
但是,当我们修改 b
中的元素时:
In [1]: b[0][0] = 2333
In [2]: b
Out[2]: [[2333, 0], [2333, 0], [2333, 0]]
看到了吗?我们只修改了 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 终端中看一下 b
的 3
行的内存地址(可以使用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
;二维列表中存放了3
个x
,即[x, x, x]
。
那 Python 二维数组的正确写法是什么呢?
第一种写法,你可以把所有的元素都显式的写出来,这样肯定没问题:
a = [[0, 0], [0, 0], [0, 0]]
第二种写法,我们使用 for
来定义第二个维度。是推荐的写法。
In [1]: a = [[0] * 2 for _ in range(3)]
In [2]: a
Out[2]: [[0, 0], [0, 0], [0, 0]]
In [3]: a[0][0] = 666
In [4]: a
Out[4]: [[666, 0], [0, 0], [0, 0]]
为什么使用 for
的写法可以呢?
因为这种情况下 [0] * 2
运行了 3
次,所以返回的是 3
个不同的地址x,y,z
。二维列表中的存放的是 [x, y, z]
。
可视化结果如下:
可以看到 3
行指向不同的地址,这样的结果就是符合预期的了。
总结
今天分享了 Python 刷题中常见的一个坑:二维列表的定义。
我们通过代码运行可视化工具,分析了为什么使用 *
定义二维数组会出问题。
最后也给出了使用 for
来定义二维列表的正确写法。
了解内存知识,在编程中非常有用。
推荐新手在遇到这种问题时,使用可视化工具进行排查,非常有助于理解。
以上就是「刷题躲坑」系列的第一篇文章啦!
欢迎关注我的公众号「负雪明烛」,在编程学习路上,我们一起踩坑、躲坑。
这是我持续坚持写作的第 5/7 天。
面试最常考的 100 道算法题分类整理! LeetCode 最经典的 100 道题 直播分享:LeetCode 应该怎么刷? 我的爆款算法题解是怎么创作出来的? 写了 1000 篇算法题解是什么体验? 为什么「执行代码」正确,「提交」出错?