一、引题
本周在做力扣上的算法题(删除排序数组中的重复项)时,遇到了超出时间限制的问题,后来才知道是我设计的算法时间复杂度过高,于是我就对list的各个基本操作和常用函数的复杂度作了个了解。
二、背景知识
1.数组是一种线性表结构,其用一块连续的内存空间,来存储一组具有相同类型的数据
2.时间复杂度,也叫做渐进时间复杂度,通常用大O公式书写,表示代码的执行时间随数据规模增长的变化趋势,而非真正的执行时间。因此大O关注的是变化趋势。
三、列表(list)特点
1.底层基于数组实现
python list本质上是一个over-allocate的数组,啥叫over-allocate呢?就是当底层数组容量满了而需要扩充的时候,python依据规则会扩充多个位置出来。比如初始化列表array=[1, 2, 3, 4],向其中添加元素23,此时array对应的底层数组,扩充后的容量不是5,而是8。这就是over-allocate的意义,即扩充容量的时候会多分配一些存储空间。这样做的优点当然是提高了执行效率,否则每次添加元素,都要对底层数组进行扩充,效率是很低下的。另外,当列表存储的元素在变少时,python也会及时收缩底层的数组,避免造成内存浪费。这里可以通过对列表的实践,验证扩充与收缩的过程(通过 sizeof()或sys.getsizeof()查看内存变化,并推算容量值)。
2.属于引用数组
>>> a = [1, 2, 3, 4]
>>> a.__sizeof__()
72
>>> b = ['hello', 'world', 'mac']
>>> b.__sizeof__()
64
>>> c = [int('10'), str(8)]
>>> c.__sizeof__()
56
>>> d = 23
>>>
>>> l = []
>>> l.__sizeof__()
40
>>> l.append(a)
>>> l
[[1, 2, 3, 4]]
>>> l.__sizeof__()
72
>>> l.append(b)
>>> l.__sizeof__()
72
>>> l.append(c)
>>> l.__sizeof__()
72
>>> l.append(d)
>>> l.__sizeof__()
721
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
我们知道,初始列表的底层数组容量是0,第一次会扩充为4个元素的容量,占用32个字节,每个元素占用8个,注意这里就是每个元素占用8个字节,而不是平均的结果为8个字节。如果列表中存放实际的元素,那上面实践中列表l添加完元素列表a之后,其占用的字节就不会是72了。因此列表本质上存储的是对象的引用。
四、列表各操作时间复杂度分析
常见的时间复杂度高低排序:
O(1)
具体的看下表,'n’是容器中当前的元素数, 'k’是需要操作的元素个数。
操作
时间复杂度
index()
O(1)
append()
O(1)
extend()
O(k)
insert()
O(n)
count()
O(n)
remove()
O(n)
pop()
O(1)
pop(i)
O(n)
sort()
O(n log n)
reverse()
O(n)
len()
O(1)
max(),min()
O(n)
del(del list[i] 或者 del list[i:j])
O(n)
slice [x:y] (切片)
O(k)
iterration(列表迭代)
O(n)
in 关键字
O(n)
通过分析可以发现,列表不太适合做元素的遍历、删除、插入等操作,对应的时间复杂度为O(n);访问某个索引的元素、尾部添加元素或删除元素这些操作比较适合做,对应的时间复杂度为O(1)。
其他集合内置方法的时间复杂度
最后简单了解一下空间复杂度:
空间复杂度是用来评估算法内存占用大小的方式。定义一个或多个变量,空间复杂度都是为1;列表的空间复杂度为列表的长度。
a = 'Python' #空间复杂度为1
b = 'PHP'
c = 'Java'
num = [1, 2, 3, 4, 5] #空间复杂度为5
num = [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]] #空间复杂度为5*4
num = [[[1, 2], [1, 2]], [[1, 2], [1, 2]] , [[1, 2], [1, 2]]] #空间复杂度为3*2*21
2
3
4
5
6
7