接下里的系列文章中,我们将讲一下Python高性能计算,如何提高Python的计算性能?有哪些好用的库?请持续关注我们。第一节我们先来讲列表。

Python列表是有序的元素集合,在Python中是使用大小可调整的数组实现的。数组是一种基本的数据结构,由一系列连续的内存单元组成,其中每个内存单元都包含指向一个Python对象的引用。

列表在访问、修改和增加元素方面,速度都非常快。其中访问和修改元素,都需要从底层数组的相应位置获取对象引用,因此其复杂度是O(1)。增加元素的话,跟C++中的Vector原理类似(C++ vector用法详解),当创建一个空列表时,将分配一个长度固定的数组;当在列表末尾插入元素时,复杂度为O(1),数组中的位置逐渐被填满。当所有位置都被占据后,列表需要增大其底层数组的长度,进而触发内存的重新分配,这需要的时间为O(N)。但内存分配的操作并不频繁,因此在列表尾部增加元素的时间复杂度接近于O(1).

在列表开头或中间增加或删除元素的效率就要慢很多了,这也很容易理解,后续所有的元素都要移动一个位置,所以时间复杂度是O(N)。因此,列表的几个常规操作的时间复杂度如下:操作时间复杂度

list.pop()O(1)

list.pop(0)O(N)

list.append(1)O(1)

list.assert(0, 1)O(N)

在某些场景下,我们需要频繁的对列表的头和尾进行操作,这时我们就可以使用双端队列collections.deque,这种数据结构被设计成能够在集合两端高效地增加和删除元素,在Python中,双端队列是以双向链表的方式实现的。deque的常用操作如下:

IPython中的示例代码如下:
from collections import deque
d = deque(range(10))
>>> deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
d.pop()
>>> 9
d.popleft()
>>> 0
d
>>> deque([1, 2, 3, 4, 5, 6, 7, 8])
d.rotate(2)
>>> deque([7, 8, 1, 2, 3, 4, 5, 6])
deque操作头和尾的时间复杂度都是O(1).操作时间复杂度
deque.pop()O(1)
deque.popleft()O(1)
deque.append()O(1)
deque.appendleft()O(1)
虽然deque操作头和尾的时间复杂度都是O(1),由于list,但我们平时却不太会使用双端队列。主要是因为除了头和尾,访问双端队列中间的元素所需的时间为O(N)。list和deque访问时间复杂度
list[0]O(1)
list[N/2]O(1)
list[N-1]O(1)
deque[0]O(1)
deque[N/2]O(N)
deque[N-1]O(1)

列表查找元素通常是一种O(N)操作,使用list.index()来完成。为了提高列表的查找速度,一种简单的办法是确保底层数组是有序的,并使用模块bisect来执行二分查找。

import bisect

lst = [10, 11, 13, 14]

bisect.bisect(lst, 12)

>>> 2

bisect.bisect(lst, 13)

>>> 3

可以看到,使用bisect可以直接得出这个值应该插在什么位置,如果该值(13)本来就在列表中,默认返回的位置是列表中该元素后面的位置(3)。当然我们可以使用bisect_left函数,这样可以返回该元素前面的位置。

bisect.bisect_left(lst, 13)
>>> 2
bisect.bisect_right(lst, 13)
>>> 3

bisect是使用二分法查找,时间复杂度是O(log(N)),在数据量较大时,比list.index()的O(N)复杂度要节省出不少的时间。