< Data Structures andAlgorithms in Python > Michael T.Goodrich,Roberto Tamassia,Michael H.Goldwasser 学习笔记
python中创建数组,计算机系统先会创建一个低层次数组,以便系统为其存储分配连续内存,通常情况下,底层数组会比列表的长度更长。当我们声明一个列表list,并向其中添加元素,一旦元素数量超过底层数组的大小,列表类会向系统重新申请一个更大的底层数组,并使这个更大的底层数组的前面部分和原来的数字一样。这时原来的数组就不需要了,会给系统回收,这样一来,可以继续向列表中添加元素了(元组和字符串不需要考虑,因为元组和字符串对象实例化后就不可变,底层数组大小也就确定了)。如下代码及其结果给出证明:
import sys
data = []
n = 20
for k in range(n):
a = len(data)
b = sys.getsizeof(data)
print('Length:{0:3d};Size in bytes:{1:4d}'.format(a,b))
data.append(None)
sys模块中的getsizeof函数用于给出python中存储对象的字节数
在上面图片中可以看出,一个空列表占了64个字节的内存。python中每个列表对象除了存储元素之外,还需要存储一些状态,比如说列表当前存储的元素个数等。当我们添加第一个元素的时候,字节数从64调到了96,增加了32个字节,由于本实验是在64位机器上运行的,32个字节可以存储4个对象引用。这和我们之前所述一致,当对列表添加第2、3、4个元素的时候,占用的内存大小并没有变化,以此类推当我们添加第5、9、17个元素的时候,系统都会给列表重新分配新的底层数组,以便存储更多的元素。
实现动态数组
当底层数组已满时,需要添加元素,系统会进行如下操作:
- 分配一个更大的数组;
- 令;
- 令,即之后使用来代替;
- 释放原数组所占用的内存。
创建数组
把A中元素存入
释放内存
摊销分析
根据以上分析,data.append(None)操作的时间复杂度不是每次都是,当底层数组已满时,进行data.append(None)操作,其时间复杂度就会变为,n为原来底层数组的大小。所以列表append操作的时间复杂度和底层数组的大小有关系,如果底层数组大小无限大,那append操作的时间复杂度就是,但是这样会占过多的内存,明显不可取。每次底层数组存满,创建一个多大的底层数组来替换原先的底层数组会很大程度地影响append的性能。我们需要在运行效率和内存使用之间选取有个折中的方案。
大小按几何增长
假如当前底层数组大小为c,当底层数组存满时,系统创建新的底层数组,其大小为2c(c的倍数)。我们能够证明这样的机制摊销运行时间为,证明如下:
我们假设底层数组初始大小为c,每次增长原来底层数组的2倍。第一次添加c个元素,其时间复杂度为,再添加c个元素,其时间复杂度为(底层数组满时,需要创建数组并将原来的数据存入新数组,时间复杂度为,再加上之后添加的c个元素,其时间复杂度为),再添加2c个元素,其时间复杂度为,再添加8c个元素,其时间复杂度为,以此类推。
如图:
添加元素 | c | c | 2c | 4c | 8c | …… |
添加时间复杂度 | O(c) | O(2c) | O(4c) | O(8c) | O(16c) | …… |
总元素n | c | 2c | 4c | 8c | 16c | …… |
总时间复杂度 | O(c) | O(3c) | O(7c) | O(15c) | O(31c) | …… |
根据上面的两个公式,我们可以得出时间复杂度为
因此,每个添加操作的摊销运行时间为
避免使用固定增量
设固定增量为c,和上面分析的一样:设起始为空底层数组,大小为c,每添加c个元素所花费的时间为c,2c,3c,……,mc,总共添加了个元素。其总共花费的时间为
因此,执行个append操作花费的时间为,这个的性能明显几何增长的要差。
Python列表的效率
data、data1、data2的长度分别为、、
操作 | 运行时间 |
data[j] = val | |
data.append(value) | |
data.insert(k,value) | |
data.pop() | |
data.pop(k) del data[k] | |
data.remove(value) | |
data1.extend(data2) data1 += data2 | |
data.reverse() |
* 摊销 :改变数组大小的操作,有些情况下需要改变底层数组,所以需要进行摊销的操作。