C++ STL 双端队列原理与优先级队列模拟实现
前言
本文聚焦于 STL 中的双端队列(deque)与优先级队列。我们将深入剖析 deque 如何融合 vector 与 list 的优势,通过中控数组与分段缓存实现高效头尾操作;结合优先级队列的堆结构,详解仿函数在自定义排序规则中的核心作用。通过模拟实现代码与性能对比,帮助大家理解这些容器适配器的底层逻辑。
一、deque(双端队列)
1.1 list 和 vector 的优缺点
在讨论 deque 之前,我们先回顾一下 vector 和 list 的特性,这有助于理解 deque 存在的意义。
vector 的优点:
- 尾插尾删效率高。
- 支持高效的下标随机访问。
- 物理空间连续,高速缓存利用率高。
vector 的缺点:
- 空间需要扩容,扩容代价较高(涉及内存拷贝和释放)。
- 头部和中间的插入删除效率低,需要搬移元素。
list 的优点:
- 按需申请释放空间,不需要整体扩容。
- 支持任意位置的插入删除。
list 的缺点:
- 不支持下标的随机访问。
deque 可以看作是这两者的结合体,它既保留了 vector 的随机访问能力,又具备了 list 在头尾操作上的灵活性。
1.2 deque 的原理介绍
dque(双端队列)是一种双开口的"连续"空间数据结构。所谓的"连续"是指逻辑上的连续,允许在头尾两端进行 O(1) 时间复杂度的插入和删除操作。
底层结构: dque 并非真正连续的内存空间,而是由一段段连续的小空间拼接而成。实际结构类似于一个动态的二维数组:
- 中控数组(Map):本质是一个指针数组,用于管理各个缓冲区的地址。
- 缓冲区(Buffer):实际存储数据的一段段连续内存。
当缓冲区满了之后,会开辟新的缓冲区,并将新缓冲区的地址存入中控数组。为了维护逻辑上的连续性,deque 的迭代器设计较为复杂,需要处理跨缓冲区的跳转。
迭代器机制:
dque 的迭代器包含四个关键成员:cur(当前指向位置)、first(当前缓冲区起始)、last(当前缓冲区结束)、node(指向中控区存放该数组的地址)。
operator++:判断当前缓冲区是否走完。若未走完,直接cur++;若走完,则通过node+1找到下一个缓冲区,更新first和last,并将cur重置为下一个缓冲区的起始位置。
性能分析:
- 头插尾插:效率高于 vector 和 list。list 每次只申请一块,而 deque 一次申请一段空间(如 40 字节),比多次申请小块更高效;vector 扩容代价大,而 deque 只需新增缓冲区并调整指针,仅在中控区满时才需扩容(仅拷贝指针)。
- 随机访问:效率不错,但略低于 vector,因为需要计算索引对应的缓冲区偏移量。
- 中间插入删除:效率为 O(n),因为涉及缓冲区的移动或扩容。
由于栈和队列通常不涉及中间元素的频繁处理,使用 deque 作为默认适配容器是非常合理的选择。
1.3 deque 和 vector 的性能对比
在 Release 版本下,对 100 万数据进行排序测试,结果如下:
{
(());
N = ;
deque<> dq;
vector<> v;
( i = ; i < N; ++i) {
e = () + i;
v.(e);
dq.(e);
}
begin1 = ();
(v.(), v.());
end1 = ();
begin2 = ();
(dq.(), dq.());
end2 = ();
(, end1 - begin1);
(, end2 - begin2);
}


