查看: 77|回复: 0

Python deque双端队列用法、性能选型与滑动窗口实战

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
Python 的 collections.deque 是标准库里的双端队列实现,通常读作“deck”。它允许在序列两端插入和删除,既能当栈用,也能当队列用,可以理解为栈和队列的超集。很多人会拿它和 list 比较:list 在头部插入或删除时要搬移后续元素,复杂度是 O(n);deque 的两端操作只需要调整指针,复杂度是 O(1)。这是 deque 存在的核心理由,也是选型时最需要抓住的点。

一、核心 API 与容易踩的顺序坑

导入方式:
  1. from collections import deque
  2. dq = deque([1, 2, 3])
  3. dq.append(4)       # 尾部添加
  4. dq.appendleft(0)   # 头部添加
  5. dq.pop()           # 删除并返回尾部元素
  6. dq.popleft()       # 删除并返回头部元素
复制代码

append 对应普通队列入队,appendleft 从头部插入,底层只动头指针,代价是常量级。空队列上调用 pop 或 popleft 会抛 IndexError,生产代码里最好先判断 if dq:,或者用 try/except 接住。

批量操作里有 extend 和 extendleft,后者最容易出错:
  1. dq = deque([1, 2, 3])
  2. dq.extend([4, 5])
  3. dq.extendleft([-1, -2])
  4. # 结果:deque([-2, -1, 1, 2, 3, 4, 5])
复制代码

extendleft([-1, -2]) 之后头部依次是 -2、-1,因为内部等价于逐个调用 appendleft,每次新元素都塞到最前面。想保持原顺序,不要直接用 extendleft。

特殊能力包括 rotate、remove、count、index、clear、copy 和 maxlen。例如:
  1. dq = deque([1, 2, 3, 4, 5])
  2. dq.rotate(2)    # deque([4, 5, 1, 2, 3])
  3. dq.rotate(-1)   # deque([5, 1, 2, 3, 4])
  4. dq.remove(3)
  5. dq.count(2)
  6. dq.index(4)
复制代码

rotate 正数向右循环移动,负数向左循环移动,在轮转调度、约瑟夫环、日历翻页里很顺手。remove 从左往右删除第一个匹配项,找不到会抛 ValueError。index、count 也是从左往右扫描。

二、四个典型算法场景

回文检测是双端队列最直观的案例:
  1. def is_palindrome(s: str) -> bool:
  2.     dq = deque(s.lower())
  3.     while len(dq) > 1:
  4.         if dq.popleft() != dq.pop():
  5.             return False
  6.     return True
  7. assert is_palindrome('racecar') is True
  8. assert is_palindrome('hello') is False
  9. assert is_palindrome('上海自来水来自海上') is True
复制代码

这个写法用 popleft 和 pop 成对比较两侧字符,索引细节少,出错概率低。纯字符串回文用 s == s[::-1] 更快,但作为理解双端操作的示例更合适。

滑动窗口最大值是单调双端队列的经典应用。deque 里存窗口元素下标,并保证对应值从左到右单调递减,队首就是当前窗口最大值下标。每一步先清理过期队首,再从队尾弹出所有不大于当前值的下标,最后压入当前下标:
  1. from collections import deque
  2. def max_sliding_window(nums, k):
  3.     dq = deque()
  4.     res = []
  5.     for i, v in enumerate(nums):
  6.         while dq and dq[0] <= i - k:
  7.             dq.popleft()
  8.         while dq and nums[dq[-1]] <= v:
  9.             dq.pop()
  10.         dq.append(i)
  11.         if i >= k - 1:
  12.             res.append(nums[dq[0]])
  13.     return res
复制代码

顺序很重要:先清理窗口之外的,再维护单调性,最后 append 当前下标。顺序错了,结果会时对时错。这个“丢弃未来不可能再用的元素”的思路,也能迁移到其他优化问题。

BFS 应该默认用 deque,而不是 list.pop(0):
  1. def bfs(root):
  2.     if not root:
  3.         return []
  4.     res = []
  5.     dq = deque([root])
  6.     while dq:
  7.         node = dq.popleft()
  8.         res.append(node.val)
  9.         if node.left:
  10.             dq.append(node.left)
  11.         if node.right:
  12.             dq.append(node.right)
  13.     return res
复制代码

小规模图上看不出差异,节点上万后,list 头部弹出 O(n) 的代价会体现到秒级。双向 BFS 也可以用两个 deque 分别维护起点和终点扩展,每次从节点数更少的一端扩展一层,再用集合记录已访问节点判断相交。

约瑟夫环用 rotate 写非常直白:
  1. def josephus(n, k):
  2.     dq = deque(range(1, n + 1))
  3.     while len(dq) > 1:
  4.         dq.rotate(-(k - 1))
  5.         dq.popleft()
  6.     return dq[0]
复制代码

josephus(7, 3) 的结果是 4。rotate(-(k-1)) 把第 k 个人转到队首,再 popleft 移除。任务轮转调度也可以用 rotate(-1) 模拟当前任务出队再入队。

三、底层实现与复杂度真相

CPython 中 deque 的底层是“块状双向链表”。多个 block 之间用双向链表串起来,每个 block 内部是一段连续存储元素的定长数组。block 容量与机器指针大小有关,CPython 源码里有一个基于 sizeof(void*) 计算的常量,通常约 64。块满时新分配 block 挂到链表末尾,块空时释放或回收。

这种结构让两端操作只需调整首尾 block 里的指针和元素,不需要像 list 那样搬移整片连续内存。但中间索引不是 O(1):dq 要从头部或尾部沿链逐个块找过去,CPython 会根据下标离哪一端更近来决定遍历方向,平均仍是 O(n)。remove、index、count 同理,最坏都是 O(n)。一句话:deque 的两端是高速公路,中间是乡间小路。

线程安全方面,deque 的单个原子操作在 CPython 中受 GIL 保护,append、appendleft、pop、popleft 不会被其他线程打断,可以用于生产者消费者模型。但 if dq: item = dq.popleft() 这种“检查后操作”的组合不是原子的,需要自己加锁,或者使用 queue.Queue。queue.Queue 本质上是在 deque 之上包了一层线程安全机制和阻塞通知,put、get、task_done、join 提供完整多线程语义。

四、性能实测与容器选型

可以用 timeit 对比 list.pop(0) 和 deque.popleft():
  1. import timeit
  2. setup_list = 'lst = list(range(100000))'
  3. setup_deque = 'from collections import deque; dq = deque(range(100000))'
  4. t_list_pop0 = timeit.timeit('lst.pop(0)', setup=setup_list, number=10000)
  5. t_deque_popleft = timeit.timeit('dq.popleft()', setup=setup_deque, number=10000)
  6. print(f'list.pop(0): {t_list_pop0:.4f} s')
  7. print(f'deque.popleft(): {t_deque_popleft:.4f} s')
复制代码

注意 number=10000 很容易让 list 越界,因为同一个 list 弹 10000 次已经空了。更严谨的测试应每轮重置容器,这里只是演示差异。实际运行时,list 的耗时在几十毫秒级别震荡,deque 通常稳定在几毫秒以下;数据量到百万级后,list 的劣势会指数级放大,deque 依旧平稳。头部 insert(0, x) 和 appendleft 也是同样结论。尾部操作两边都是 O(1),差距可忽略。

maxlen 是有界队列的隐藏利器:
  1. dq = deque(maxlen=3)
  2. for i in range(5):
  3.     dq.append(i)
  4. # 结果:deque([2, 3, 4], maxlen=3)
复制代码

达到上限后再插入,另一端的旧元素会自动挤出。日志缓存、实时数据流最近 N 条记录都很适合。maxlen 一旦设定不能修改,想改只能重建 deque。

选型可以按操作重心判断:随机访问多、中间插入多、数据量小,用 list,连续内存缓存命中高,索引 O(1);高频头部插入删除,用 deque,两端 O(1);BFS、滑动窗口、单调队列,用 deque;线程间安全传递任务,用 queue.Queue;按优先级取元素,用 heapq 或 PriorityQueue;日志缓存、最近 N 条记录,用 deque(maxlen=N)。deque 和 list 可以互转,实际开发中常把 deque 当临时缓冲区,处理完一批数据再 list(dq) 转成列表做随机访问或传给下游。

五、常见问题与避坑

不要把 deque 当 list 做随机访问。for i in range(len(dq)): dq 这种写法是 O(n^2),改成 for line in dq 迭代才是 O(n)。需要索引访问时先转 list。deque 也不能直接切片,会报 TypeError: sequence index must be integer, not slice,可以自己写 itertools.islice 或转 list 再切。

deque 不能直接被 json.dumps 序列化,会抛 TypeError: Object of type deque is not JSON serializable。解决方式是在序列化前转 list:
  1. import json
  2. from collections import deque
  3. dq = deque([1, 2, 3])
  4. data = json.dumps(list(dq))
  5. # 反序列化
  6. dq2 = deque(json.loads(data))
复制代码

pickle 可以直接序列化 deque,不需要额外转换。深拷贝方面,deque 和 list 一样是可变容器,dq.copy() 是浅拷贝,只复制容器本身,里面的可变对象仍会互相影响,嵌套结构要用 copy.deepcopy(dq)。

面试和考试里还有受限双端队列考点。输入受限双端队列指一端可插入、可删除,另一端只允许删除;输出受限则相反,一端可插入、可删除,另一端只允许插入。常见题是给输入序列,问哪些输出序列合法。另一个高频题是用两个栈模拟双端队列,核心是把入队操作分散在两个栈上,弹出时从对应栈取,栈空时从另一个栈搬运。还要注意,deque 和 collections.defaultdict 没有关系,只是名字容易让人混淆。

实际写代码时,只要看到“头部插入或删除”的动作,就可以优先考虑 deque,不要等数据量上来再重构。需要旋转列表时,也先想想 rotate 能否直接解决。若只是写不追求性能的小脚本,list 完全够用,合适才是最重要的。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-23 12:05 , Processed in 0.020216 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部