一位同事最近编写了一个程序,他使用了 Python 列表作为队列。换句话说,他需要插入项目时使用 .append(x),在需要删除项目时使用 .pop(0)。
我们知道 Python 具有 collections.deque,我们正在试图弄清楚是否应该花费有限的时间来重写此代码以使用它。假设我们执行数百万次追加和弹出操作,但永远不会超过几千个条目,那么他的列表使用会有问题吗?
具体来说,Python 列表实现中使用的基础数组是否继续无限增长,即使列表只包含一千个内容,或者 Python 最终会重新分配并释放其中一部分内存?
2、解决方案
方法 1:微基准测试比较
使用 Python 的 timeit 模块进行微基准测试,比较列表和 deque 在不同数量的元素下的插入和删除性能。
结果显示,deque 在所有情况下都比列表快得多。当有 1000 个元素时,deque 的速度大约是列表的 2 倍,当有 10,000 个元素时,deque 的速度大约是列表的 10 倍。
import timeit
import collections
# 测试插入和删除 1000 个元素
time_list = timeit.timeit('q.append(23); q.pop(0)', 'q=range(1000)')
time_deque = timeit.timeit('q.append(23); q.popleft()', 'import collections; q=collections.deque(range(1000))')
# 打印结果
print('List: %.6f seconds' % time_list)
print('Deque: %.6f seconds' % time_deque)
# 测试插入和删除 10000 个元素
time_list = timeit.timeit('q.append(23); q.pop(0)', 'q=range(10000)')
time_deque = timeit.timeit('q.append(23); q.popleft()', 'import collections; q=collections.deque(range(10000))')
# 打印结果
print('List: %.6f seconds' % time_list)
print('Deque: %.6f seconds' % time_deque)
微基准测试结果表明,对于频繁的插入和删除操作,deque 比列表具有显著的性能优势。
方法 2:使用 collections.deque
从 Python 2.3 开始, collections.deque 对象是专门为双端队列操作而设计的。它提供了与列表对象非常相似的 API,但它在插入和删除操作方面更加高效,因为它的底层实现利用循环缓冲区。
要使用 collections.deque,可以按照以下步骤操作:
- 导入 collections 模块:
import collections
- 创建一个 collections.deque 对象:
my_deque = collections.deque()
- 使用 append() 和 popleft() 方法来插入和删除元素:
my_deque.append(1)
my_deque.append(2)
my_deque.append(3)
item = my_deque.popleft() # 返回 1
item = my_deque.popleft() # 返回 2
item = my_deque.popleft() # 返回 3
方法 3:使用列表并注意性能问题
如果确实需要使用列表作为队列,则需要注意以下性能问题:
- 在列表的开头插入或删除元素需要 O(n) 的时间复杂度,因为所有后续元素都必须重新分配。
- 在列表的末尾插入或删除元素需要 O(1) 的时间复杂度,因为不需要移动任何元素。
- 如果经常在列表的开头插入或删除元素,则列表可能会变得非常碎片化,从而导致性能下降。
为了减轻这些性能问题,可以使用以下技巧:
- 尽量在列表的末尾插入或删除元素。
- 如果需要经常在列表的开头插入或删除元素,可以使用双端队列(deque)数据结构,它可以在 O(1) 的时间复杂度内在两端插入或删除元素。
- 使用列表时,注意内存使用情况,避免列表变得过大。