一、数据结构说明
- queue
一种先进先出的队列结构(FIFO),在stl里面的queue是一种适配器,也就是对于具体底层结构的封装。听不懂没关系,知道stl里面的queue一般不直接使用,而是用deque或者priority_queue代替就可以。默认的底层实现时deque - deque
deque的数据集结构是一种双向队列,允许在存储数据的两侧高效的插入/删除,也可以用来模拟stack(把一端的插入删除禁止了就行),两边插入删除的时间都是O(1),中间插入删除则是O(n),不过由于这种双端的特性,操作的开销是略大于vector的。 - priority_queue
优先级队列或者叫堆都可以。简单来说只保证可以快速访问/移除最高优先级元素的数据结构,默认的是最大堆。
二、常见使用方法
C++
//构造上和vector差不多
// 常见构造
std::deque<int> d; // 空 deque
std::deque<int> d1(10); // 包含 10 个默认值(0)
std::deque<int> d2(10, 5); // 包含 10 个值为 5 的元素
std::deque<int> d3 = {1,2,3,4}; // 列表初始化
std::deque<int> d4(d3); // 拷贝构造
std::deque<int> d5(std::move(d4)); // 移动构造
// 查看属性
d.size(); // 元素个数(size_type)
d.empty(); // 是否为空
// deque 没有 capacity() 接口(因为底层是分段数组,capacity 概念不同)
// 但可以通过 d.max_size() 查看理论上能容纳的最大元素数(实现相关)
// 改容量和大小(与 vector 不完全相同)
d.resize(n); // 改变 size:变大时填充默认值(或指定值),变小时移除多余元素
d.clear(); // 清空元素,但底层块可能保留以便复用(capacity 不可见)
d.shrink_to_fit(); // 有些实现对 deque 支持有限,行为非必须(实现相关)
// 元素增删改查
d.push_back(x); // 尾部添加 (amortized O(1))
d.pop_back(); // 删除尾部元素 (O(1))
d.push_front(x); // 头部添加 (amortized O(1))
d.pop_front(); // 头部删除 (O(1))
d[i]; // 随机访问 (O(1))
d.at(i); // 带范围检查的访问,越界抛 out_of_range
d.front(); d.back(); // 访问首/尾元素(可读写)
d.insert(it, x); // 在迭代器 it 位置插入(O(n))
d.erase(it); // 擦除 it(返回下一个元素迭代器,O(n))
d.erase(begin, end); // 区间擦除(O(n))
// emplace 系列
d.emplace_back(args...); // 直接在尾部构造(减少拷贝/移动)
d.emplace_front(args...);
// 遍历
for (size_t i = 0; i < d.size(); ++i)
d[i] = static_cast<int>(i);
for (const auto &x : d)
std::cout << x << ' ';
// 也可用迭代器,但是用得比较少,一般是范围for循环或者直接for
for (auto it = d.begin(); it != d.end(); ++it) { *it = *it + 1; }
// 排序(对元素重新排序),更换比较函数的话同vector
std::sort(d.begin(), d.end()); // deque 支持随机访问迭代器
std::stable_sort(d.begin(), d.end()); // 稳定排序(额外内存)
// 注意:排序会移动/拷贝元素,可能触发移动构造/拷贝构造
关于priority_queue也是差不多
// priority_queue 构造方式与 vector 等普通容器不太相同,不直接支持 range, copy, move 等
// 常见构造
std::priority_queue<int> pq; // 空的默认最大堆(top 最大)
std::priority_queue<int, std::vector<int>, std::greater<int>> pq_min; // 空的最小堆(top 最小),greater是内置的比较函数,就是升序变降序
std::priority_queue<int> pq_from_vec({1,2,3,4}); // 列表初始化,内部 O(N) 构建堆
// 从现有容器构造 (O(N) 性能)
std::vector<int> v = {3,1,4,1,5};
std::priority_queue<int> pq_init(v.begin(), v.end()); // 使用迭代器范围构造最大堆
// 查看属性
pq.size(); // 元素个数
pq.empty(); // 是否为空
// priority_queue 没有 capacity() 接口,因为其本质是适配器
// priority_queue 没有 resize() / clear() 这种直接改大小和容量的接口
// 若需清空,只能不断 pop
while(!pq.empty()) pq.pop(); // 清空所有元素
// 元素增删查
pq.push(x); // 插入元素,O(log N)
pq.emplace(args...); // 原位构造并插入元素,O(log N)
pq.pop(); // 弹出 top 元素,O(log N)
pq.top(); // 访问 top 元素(只读),O(1)
// priority_queue 不支持随机访问 (pq[i] 或 pq.at(i)),也不支持 insert/erase 中间元素
// 遍历,一般不使用,都是直接不断pop
// priority_queue 不提供迭代器,不能直接使用范围 for 或迭代器循环
// 遍历需拷贝一份,然后不断 pop
// std::priority_queue<int> temp_pq = pq;
// while(!temp_pq.empty()) { std::cout << temp_pq.top() << " "; temp_pq.pop(); }
// 排序,不支持
// priority_queue 不支持 std::sort,因为其不提供随机访问迭代器
// 若需要有序输出,只能通过不断 pop 来获取排好序的元素(从大到小或从小到大)
三、底层实现
- deque。主要通过
多个固定大小的块(block)实现,每个块可以容纳B个元素(看具体实现),这些块的指针存放在一个map(这里的map只是个简单的数组,不是stl的map)。结构上是块内连续,块间不连续。也正因为这种结构,块中间的插入删除需要的时间复杂度来到O(n),随机访存虽然也是O(1)但是比vector低效一点。 - priority_queue。默认的底层是vector,通过堆生成算法来实现逻辑结构,二叉堆的插入和删除参考二叉堆算法,时间复杂度都是O(long(n)),不过全部都出栈算上调整的时间就来到了O(log(n)),在实际的使用基本只用来找有限个的最值
四、注意事项
由于deque不完全连续的实现思想,在插入/删除时,如果涉及到新块扩容(增加一个新的block)有可能导致迭代器失效,建议稳妥的做法时涉及到频繁的删除就用list链表,别用的队列实现,或者分两边操作,第一遍只遍历检查,确定要删除的数据,第二遍开始从后往前删除(一般情况不建议删除,加个标记表示这个数据失效就行,比如用一个set或者数组记录哪些元素失效)。