原创不易,转载前请注明博主的链接地址:Blessy_Zhu https://blog.csdn.net/weixin_42555080
本次代码的环境:
运行平台: Windows
Python版本: Python3.x
IDE: PyCharm
一、 问题描述
今天在看Magnus Lie Hetland写的《Python算法教程》时,偶然间看到了一个自己一直忽略的问题,即Python的list列表的本质。
二、 代码展示
我们还是先来看代码:
1、append:对列表的操作
重点看运行时间,结果如图1。
import time
start = time.perf_counter()
count = 10**5
nums = []
for i in range(count):
nums.append(i)
nums.reverse()
end = time.process_time()
print('Running time: %s Seconds'%(end-start))
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/26bc30b20d0b3408ddcc938ecf0080f8.png)
图1
2、insert对列表的操作
重点看运行时间,结果如图2。
import time
start = time.perf_counter()
count = 10**5
nums = []
for i in range(count):
nums.insert(0,i)
end = time.process_time()
print('Running time: %s Seconds'%(end-start))
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/4630004ef5557ac672f68692348bfa95.png)
图2
从图中我们可以看到,同样是Python中的列表做增加元素的操作,两种操作的运行时间的差距确实明显。似乎是出于某种原因,当我们选择将相关元素项添加到list尾端时,其在应对list 大小变化时的性能弹性要比在其首端插入要好一些。而且,这些内置操作通常还都是用C编写的。如果我们花点时间用纯Python重新实现下 list.append方法,(粗略)估计新版本会比原版本慢50倍左右。并且我们还可以做进步估计,相较于这个较慢的纯Python实现的append方法在一台速度非常慢的机器上的表现而言,那个较快的、优化版的insert方法在一台普通计算机上的速度大致要快上1000倍。那么,如果我们现在将insert版所具有的速度优势设置为50000一个单位, 然后对比这两个实现在插入100000个数字时的情况, 您认为会是什么结果?
从直觉上来看,似乎显然应该是速度快的那个解决方案会胜出。但在这里,其“速度性”只是一个常数单位,而且它的运行时间增长得又比“较慢”的那一版要快一些。 就眼前这个例子来说,运行环境较慢的、用纯Python 实现的那版代码完成时间实际 上只有其他版本的一半。下面让我们再继续扩大问题的规模,如将数字增加到1 000万个。这时候我们会发现,慢机器上的Pyton版本(append方法)已经比快机器上的C版本(inset方法)快了近2000倍"。这中间的区别几乎就相当于其中一个只需运行不到一分钟的时间, 而另一个则需要运行近一 天半!
三、原因讲解
其实,Python 中的list 并不是我们传统意义上的列表,这也是其append操作会比insert 操作效率高的原因所在。传统列表——通常也叫作链表( linkedlist)——通常是由一系列节点来实现的,其每个节点(尾节点除外)中都持有一个指向下一节点的引用。简单实现起来应该就像下面这样:
class Node:
def _init_ (self, value, next=None):
self.value = value
self.next = next
接下来,我们就可以将所有的节点构造成一个列表了:
>>>L = Node("a", Node("b", Node("c", Node("d"))))
>>> L. next. next. Value
'c'
这是 一个单向链表。 Python中的list则与此有些不同。它不是由若千个独立的节点相互引用而成的,而足整块单一连续的内存区块一 我们通常称之为数组array)。这直接导致了它与链表之间的一些重要区别。例如,尽管两者在遍历时的效率相差无几(除了对于链表来说有一些额外的开销),但如果我们要按既定索引值对某元素进行直接访问的话,显然使用数组会更有效率。因为在数组中,我们通常可以直接计算出目标元素在内存中的位置,并能对其进行直接访问。而对于链表来说,我们必须从头开始遍历整个链表。
但具体到insert操作上,情况又会有所不同。对于链表而言,只要知道了要在哪里执行insert 操作,其操作成本是非常低的。无论该列表中有多少元素,其操作时间(大致上)是相同的。而数组就不一样了,它每次执行insert 操作都需要移动插入点右边的所有元素,甚至在必要时,我们可能还需要将这些列表元素整体搬到一个更大的数组中去。也正因为如此,append操作通常会采用一种被称为动态数组或向量”的特定解决方案。其主要想法是将内存分配得过大一些,并且等到其溢出时,在线性时间内再次重新分配内存。但这样做似乎会使得append变得跟insert一样糟糕。其实不然,因为尽管这两种情况都有可能会迫使我们去搬动大量的元素,但主要的不同点在于,对于append操作,发生这样的可能性要小得多。事实上,如果我们能确保每次所搬入的数组都大过原数组一定的比例(例如大20%甚至100%),那么该操作的平均成本(或者说得更确切一些,将这些搬运开销均摊到每次ppend操作中去)通常是常数的。
四、 总结
以上内容介绍了append与insert的列表操作的区别,其实归根到底是一个时间复杂度O(?)的问题,当我们要插入n的量级的数据时,append的时间复杂度是O(n),而insert的时间复杂度是O(n2),这样就会造成insert操作列表的时间消耗更多。
PS:以上内容参考Magnus Lie Hetland的《Python算法教程》