《C++初阶之STL》【stack/queue/priority_queue容器适配器:详解 + 实现】(附加:deque容器介绍)
【stack/queue/priority_queue容器适配器:详解 + 实现】目录
- 前言:
- ------------标准接口介绍------------
- 一、栈:stack
- 标准模板库中的stack容器适配器是什么样的呢?
- 1. 栈的基本操作
- std::stack::top
- std::stack::push
- std::stack::pop
- 2. 查询栈的状态
- std::stack::size
- std::stack::empty
- 二、队列:queue
- 标准模板库中的queue容器适配器是什么样的呢?
- 1. 队列基本操作
- std::queue::front
- std::queue::back
- std::queue::push
- std::queue::pop
- 2. 查询队列状态
- std::queue::size
- std::queue::empty
- 三、优先队列:priority_queue
- 标准模板库中的priority_queue容器适配器是什么样的呢?
- 1. 优先队列的基本操作
- std::priority_queue::top
- std::priority_queue::push
- std::priority_queue::pop
- 2. 查询优先队列的状态
- std::priority_queue::size
- std::priority_queue::empty
- ------------------------
- 优先队列怎么显示定义为“小顶堆”?
- 1. 元素类型是“内置类型”
- 2. 元素类型是“自定义类型”
- ------------模拟实现展示------------
- 头文件:Stack.h
- 头文件:Queue.h
- 头文件:PriorityQueue.h
- 测试文件:Test.cpp
- 运行结果:
- ------------代码片段剖析------------
- 片段一:AdjustUp和AdjustDown方法的参数为什么进行了简化?
- 片段二:Swap(&a[child], &a[parent])的传参为什么发生了改变?
- ------------核心问题深究------------
- 一、仿函数
- 1. 什么是仿函数?
- 2. 仿函数有什么用途?
- 3. 仿函数与普通函数的区别有哪些?
- 4. 怎么让冒泡排序既可以升序又可以降序排序?
- 二、容器适配器
- 1. 什么是容器适配器?
- 2. C++中的三种标准容器适配器的对比差别?
- 为什么queue的底层容器不可以是:vector?
- 为什么priority_queue的底层容器不可以是:list?
- 为什么priority_queue的默认的底层容器是:vector?
- 3. 使用容器适配器需要注意那些事情?
- 三、序列容器:deque(双端队列)
- 1. 什么是deque?
- 2. deque的底层结构是什么样的?
- 物理存储结构
- 迭代器的设计
- 3. deque的优势是什么?
- 4. deque的致命缺陷是什么?
- 5. 为什么STL选择deque作为stack和queue的底层默认容器?
往期《C++初阶》回顾:
/------------ 入门基础 ------------/
【C++的前世今生】
【命名空间 + 输入&输出 + 缺省参数 + 函数重载】
【普通引用 + 常量引用 + 内联函数 + nullptr】
/------------ 类和对象 ------------/
【类 + 类域 + 访问限定符 + 对象的大小 + this指针】
【类的六大默认成员函数】
【初始化列表 + 自定义类型转换 + static成员】
【友元 + 内部类 + 匿名对象】
【经典案例:日期类】
/------------ 内存管理 ------------/
【内存分布 + operator new/delete + 定位new】
/------------ STL ------------/
【泛型编程 + STL简介】
【auto关键字 + 范围for循环 + 迭代器】
【string类:详解 + 实现】
【vector容器:详解 + 实现】
【list容器:详解 + 实现】
前言:
嗨~(★ω★)/小伙伴们大家好呀!✨今天是八月一日,在满怀热情迎接八月到来的同时,也让我们一同庆祝建军节!🎉🎊
今天要给大家分享的内容是 【stack/queue/priority_queue 容器适配器:详解 + 实现】(附加:deque 容器介绍) 🎯。哈哈,可能有小伙伴会好奇(⊙o⊙),这次怎么一下子介绍四个新东西呢?不过大家别担心(。•̀ᴗ-)✧,
因为 stack、queue、priority_queue 虽然名字里带有 “容器” 二字,但它们其实并不是容器~(๑¯∀¯๑),重点要放在后三个字 ——“适配器” 上(这可是 STL 六大组件之一哦)。
当然,本篇也会介绍一个新容器 deque,但我们只分析它的原理,不会涉及实现部分。所以这么看来,今天的内容对大家来说,其实比之前的要简单一些~那我们就一起加油,开始学习吧!٩(◕‿◕。)۶
(💡小提示💡:适配器就像是我们生活中的"转换插头"🔌,它们基于已有的容器进行封装,提供特定的接口,这个概念理解起来会很有趣哦~)
------------标准接口介绍------------
一、栈:stack
标准模板库中的stack容器适配器是什么样的呢?
cplusplus网站上关于C++的容器适配器stack的介绍:stack - C++ Reference
C++ 标准模板库(STL)中关于
stack容器适配器
相关知识,主要可以分为以下三个部分:
- 成员函数:stack自身定义的操作接口。
- 如:构造、判空、获取大小、访问 / 操作栈顶等,是直接操作栈的基础。
- 非成员函数重载:为stack适配的外部函数重载,拓展交互场景。
- 如:比较、IO 等运算符
- 非成员类特化:针对stack特性,对通用模板类做特化,适配泛型体系 。
- 如:哈希、仿函数适配等
函数名 | 接口说明 | 注意事项 |
---|---|---|
stack() | 构造一个空的栈对象 | 默认使用 deque 作为底层容器 |
top() | 返回栈顶元素的引用 | 1. 栈为空时调用是未定义行为 2. 有 const 和 非 const 两个重载版本 |
push(val) | 将元素 val 压入栈顶 | 1. 通过拷贝或移动构造元素 2. 可能触发底层容器扩容 |
pop() | 移除栈顶元素 | 1. 栈为空时调用是未定义行为 2. 不返回被移除的元素 |
size() | 返回栈中当前元素的数量 | 返回类型为 size_type (通常为 size_t ) |
empty() | 检查栈是否为空 | 返回 bool 类型,空栈时返回 true |
1. 栈的基本操作
std::stack::top
// stack::top
#include <iostream>
#include <stack> int main()
{std::stack<int> mystack;mystack.push(10);mystack.push(20);mystack.top() -= 5;std::cout << "现在栈顶的元素是:" << mystack.top() << '\n';return 0;
}
std::stack::push
// stack::push/pop
#include <iostream>
#include <stack> int main()
{std::stack<int> mystack;for (int i = 0; i < 5; ++i){mystack.push(i);}std::cout << "从栈中弹出元素..." << std::endl;while (!mystack.empty()){std::cout << mystack.top() << ' ';mystack.pop();}std::cout << '\n';return 0;
}
std::stack::pop
2. 查询栈的状态
std::stack::size
// stack::size
#include <iostream>
#include <stack> int main()
{std::stack<int> myints;std::cout << "初始状态下的size: " << myints.size() << '\n';for (int i = 0; i < 5; i++){myints.push(i);}std::cout << "入栈元素后的size: " << myints.size() << '\n';myints.pop();std::cout << "出栈元素后的size: " << myints.size() << '\n';return 0;
}
std::stack::empty
// stack::empty
#include <iostream> // std::cout
#include <stack> // std::stackint main ()
{std::stack<int> mystack;int sum (0);for (int i=1;i<=10;i++) mystack.push(i);while (!mystack.empty()){sum += mystack.top();mystack.pop();}std::cout << "total: " << sum << '\n';return 0;
}
二、队列:queue
标准模板库中的queue容器适配器是什么样的呢?
cplusplus网站上关于C++的容器适配器queue的介绍:queue - C++ 参考
C++ 标准模板库(STL)中关于
queue容器适配器
相关知识,主要可以分为以下三个部分:
- 成员函数:queue自身定义的操作接口。
- 如:构造、判空、访问及操作队头 / 队尾元素等基础函数。
- 非成员函数重载:为queue适配的外部函数,拓展交互场景。
- 如:比较、IO 运算符等
- 非成员类特化:针对queue特性,对通用模板类做特化,适配泛型体系 。
- 如:哈希、仿函数适配等
函数声明 | 接口说明 | 注意事项 |
---|---|---|
queue() | 构造一个空的队列 | 默认使用 deque 作为底层容器 |
T& front() | 返回队头元素的引用 (可修改) | 1. 队列为空时调用是未定义行为 2. 有 const 和 非 const 两个重载版本 |
T& back() | 返回队尾元素的引用 (可修改) | 同上 |
void push(const T& val) | 在队尾插入元素 val (拷贝构造) | 可能触发底层容器扩容 |
void pop() | 移除队头元素 | 1. 队列为空时调用是未定义行为 2. 不返回被移除的元素 |
size_type size() const | 返回队列中有效元素的数量 | 返回类型为 size_type (通常为 size_t ) |
bool empty() const | 检查队列是否为空 | 返回 true 表示空队列返回 false 表示非空队列 |
1. 队列基本操作
std::queue::front
// queue::front
#include <iostream>
#include <queue> int main()
{std::queue<int> myqueue;myqueue.push(77);myqueue.push(16);myqueue.front() -= myqueue.back(); // 77-16=61std::cout << "现在的队头元素是:" << myqueue.front() << '\n';return 0;
}
std::queue::back
// queue::back
#include <iostream>
#include <queue> int main()
{std::queue<int> myqueue;myqueue.push(12);myqueue.push(75); myqueue.back() -= myqueue.front();std::cout << "现在的队尾元素是:" << myqueue.back() << '\n';return 0;
}
std::queue::push
// queue::push/pop
#include <iostream>
#include <queue> int main()
{std::queue<int> myqueue;int myint;std::cout << "请输入一些整数(输入 0 以结束):\n";do {std::cin >> myint;myqueue.push(myint);} while (myint);std::cout << "myqueue队列中的内容是: ";while (!myqueue.empty()){std::cout << ' ' << myqueue.front();myqueue.pop();}std::cout << '\n';return 0;
}
std::queue::pop
2. 查询队列状态
std::queue::size
// queue::size
#include <iostream>
#include <queue> int main()
{std::queue<int> myints;std::cout << "初始状态下的size: " << myints.size() << '\n';for (int i = 0; i < 5; i++){myints.push(i);}std::cout << "入队元素后的size: " << myints.size() << '\n';myints.pop();std::cout << "出队元素后的size: " << myints.size() << '\n';return 0;
}
std::queue::empty
// queue::empty
#include <iostream>
#include <queue> int main()
{std::queue<int> myqueue;int sum(0);for (int i = 1; i <= 10; i++){myqueue.push(i);}while (!myqueue.empty()){sum += myqueue.front();myqueue.pop();}std::cout << "队列中的元素之和是: " << sum << '\n';return 0;
}
三、优先队列:priority_queue
标准模板库中的priority_queue容器适配器是什么样的呢?
cplusplus网站上关于C++的容器适配器priority_queue的介绍:priority_queue - C++ Reference
C++ 标准模板库(STL)中关于
priority_queue容器适配器
相关知识,主要可以分为以下三个部分:
- 成员函数:构造 / 查询 / 元素操作(top/push/pop等)
- 非成员函数重载:适配关系运算、输入输出等外部交互
- 非成员类特化:定制比较器(大 / 小顶堆)、选择底层容器(默认vector)
函数声明 | 接口说明 | 关键注意事项 |
---|---|---|
构造函数 | ||
priority_queue() | 构造空优先队列 | 默认大顶堆(less<T> )底层容器为 vector |
priority_queue(first, last) | 通过迭代器范围构造优先队列 | 自动调用 make_heap 构建初始堆结构 |
元素访问 | ||
const T& top() const | 返回堆顶元素 (优先级最高的元素) | 1. 必须保证队列非空 2. 返回常量引用禁止修改 |
修改操作 | ||
void push(const T& x) | 在队列尾部插入元素 x | 触发 push_heap 调整堆结构维持优先级规则(保证堆性质) |
void pop() | 移除堆顶元素 (优先级最高的元素) | 1. 必须先检查 empty() 2. 实际执行 pop_heap + 容器pop_back |
容量操作 | ||
size_type size() const | 返回优先级队列中有效元素的个数 | 返回类型为 size_t |
bool empty() const | 检查优先队列是否为空 | 空队列返回 true |
1. 优先队列的基本操作
std::priority_queue::top
// priority_queue::top
#include <iostream>
#include <queue> //std::priority_queue 注意:“优先队列”存在于“头文件queue”中int main()
{std::priority_queue<int> mypq;mypq.push(10);mypq.push(20);mypq.push(15);std::cout << "现在优先队列中的队头的元素是:" << mypq.top() << '\n';return 0;
}
std::priority_queue::push
// priority_queue::push/pop
#include <iostream>
#include <queue> int main()
{std::priority_queue<int> mypq;mypq.push(30);mypq.push(100);mypq.push(25);mypq.push(40);std::cout << "从优先队列中出队元素..." << std::endl;while (!mypq.empty()){std::cout << mypq.top() << " ";mypq.pop();}std::cout << '\n';return 0;
}
std::priority_queue::pop
2. 查询优先队列的状态
std::priority_queue::size
// priority_queue::size
#include <iostream>
#include <queue> int main()
{std::priority_queue<int> myints;std::cout << "初始状态下的size: " << myints.size() << '\n';for (int i = 0; i < 5; i++){myints.push(i);}std::cout << "入队元素后的size: " << myints.size() << '\n';myints.pop();std::cout << "出队元素后的size: " << myints.size() << '\n';return 0;
}
std::priority_queue::empty
// priority_queue::empty
#include <iostream>
#include <queue> int main()
{std::priority_queue<int> mypq;int sum(0);for (int i = 1; i <= 10; i++){mypq.push(i);}while (!mypq.empty()){sum += mypq.top();mypq.pop();}std::cout << "队列中的元素之和是:" << sum << '\n';return 0;
}
------------------------
优先队列怎么显示定义为“小顶堆”?
1. 元素类型是“内置类型”
现在请大家回想一下,前面这些接口函数的使用案例:
- top函数:我们依次向优先队列中添加了元素:10,20,15,最后通过 top 获取到的队头元素是 20
- push/pop函数:我们依次向优先队列中添加了元素:30,100,25,40,最后执行 pop 函数依次出队,得到的元素顺序是 100、40、30、25
看到这些案例,我相信你已经明白了:
代码
std::priority_queue<int> mypq;
定义的优先队列,遵循的优先级规则是:元素值越大,优先级越高,这样的优先队列本质上就是一个大顶堆
温馨提示
:这里有一个很大的误区!!!有一些小伙伴此时应该会有一点困惑,就是:
使用默认方式定义优先队列是大顶堆 —> 构造大顶堆时堆排序是升序 —> 咦但是上面出队的元素分明是降序的啊!!!
哈哈😄,这位小伙伴说的每句话的都是对的,但是得出的结论确实错的。
原因是:
上面的降序是依次出队的元素的顺序
构造大顶堆时堆排序是升序,指的是待排序数组中的元素从非升序变为了升序,两者并不是一件事情。
回归正题,优先队列可以定义为“小顶堆”吗?那我们要怎么将优先队列定义为“小顶堆”呢?
- 在 C++ 中,若要显式将
priority_queue
定义为小顶堆(元素值越小,优先级越高),需通过模板参数指定底层容器和比较器具体点说就是显式指定:
- 使用默认的 vector底层容器
- 使用标准库
<functional>
提供的greater<T>
比较器
注:上面博主没有写错哦,就是要显示指定上面的两种的东西,可能你会有疑问底层容器使用的就是默认的vector啦,我还显示指定个蛋啊!
哈哈O(∩_∩)O,这是因为
priority_queue
的模板参数列表要求:当你指定第三个参数(比较器)时,必须同时指定第二个参数(底层容器)
- 若不指定比较器:可省略所有模板参数,默认使用
vector
和less
- 若指定比较器:必须显式指定底层容器,否则会导致编译错误
总结:如果你想让优先队列定义为“小顶堆”,可以使用下面的定义方法:
std::priority_queue<T, std::vector<T>, std::greater<T>>
std::priority_queue<T, std::deque<T>, std::greater<T>>
代码案例:使用优先队列定义“小顶堆”(存储
内置类型
)
#include <iostream>
#include <vector>
#include <queue>
#include <functional> // 提供greater比较器,用于构建小顶堆
using namespace std;int main()
{/*------------------测试1:优先队列创建的“大顶堆”------------------*/cout << "---------测试1:优先队列创建的“大顶堆”---------" << endl;/*-------------第一步:创建“大顶堆”的一个优先队列-------------*///1.先创建一个vector容器(目的:为优先队列进行赋值)vector<int> v{ 3,2,7,6,0,4,1,9,8,5 };//2.使用默认的方式创建一个优先队列(不指定:“底层容器”+“比较器”)priority_queue<int> q1;//3.为优先队列进行赋值for (auto& it : v) //注:使用最原始逐个元素的赋值{q1.push(it);}/*-------------第二步:输出优先队列的队头元素验证“大顶堆”-------------*/cout << "优先的队列的队头元素是:" << q1.top() << endl;/* 注意事项:* 默认情况下,priority_queue使用vector作为底层容器,less<int>作为比较器* 这会构建一个"大顶堆"(即:堆顶元素为最大值)* 创建大顶堆:优先级最高的元素是最大值***//*------------------测试2:优先队列创建的“小顶堆”------------------*/cout << "---------测试1:优先队列创建的“小顶堆”---------" << endl;/*-------------第一步:创建“小顶堆”的一个优先队列-------------*//*---------方式一:迭代器范围初始化---------*///priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end()); //内部会自动构建小顶堆/*---------方式一:逐个插入方式初始化---------*/priority_queue<int, vector<int>, greater<int>> q2;for (auto& it : v){q2.push(it);}/*-------------第二步:输出优先队列的队头元素验证“小顶堆”-------------*/cout << "优先的队列的队头元素是:" << q2.top() << endl;/* 注意事项:* 通过显式指定模板参数:* 元素类型:int* 底层容器:vector<int>* 比较器:greater<int>(表示"较小的元素优先级更高")* 这会构建一个"小顶堆"(即:堆顶元素为最小值)* 创建大顶堆:优先级最高的元素是最小值***/return 0;
}
2. 元素类型是“自定义类型”
在
priority_queue
中存储自定义类型
的数据时,用户需要在自定义类型中重载 < 或 > 运算符,以明确元素的优先级规则。
核心逻辑:
priority_queue
(优先级队列)本质是堆结构,需要根据元素的优先级来调整堆的顺序。
- 对于自定义类型,编译器无法默认判断元素的大小关系
- 因此必须通过
运算符重载
或自定义比较器
告诉编译器如何比较元素的优先级
常见场景:
1. 使用默认比较器(大顶堆)
- 需求:希望自定义类型的对象形成大顶堆(优先级高的元素为 “较大” 的对象)
- 实现:重载
<
运算符,定义 “当前对象小于另一个对象” 的逻辑2. 显式指定小顶堆(或自定义比较逻辑)
- 需求:希望自定义类型的对象形成小顶堆(优先级高的元素为 “较小” 的对象),或使用其他比较规则(如:按属性倒序)
- 实现:重载
>
运算符,并搭配std::greater<自定义类型>
比较器
代码案例:使用优先队列定义“小顶堆”(
存储内置类型
)
#include <iostream>
#include <vector>
#include <queue>
#include <functional> // 提供greater比较器,用于构建小顶堆
using namespace std;/*-------------------------定义日期类-------------------------*/
class Date
{
public:/*-------------第一部分:定义友元函数-------------*///1.实现:“<<运算符重载函数”friend ostream& operator<<(ostream& out, const Date& d) //重载输出流运算符,支持直接 cout << Date 对象{out << d._year << "-" << d._month << "-" << d._day;return out;}/*-------------第二部分:定义成员函数-------------*///1.实现:“全缺省构造函数”// 构造函数:初始化日期对象,默认值为1900-01-01Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day){}//2.实现:“<运算符重载函数”bool operator<(const Date& d) const //重载 < 运算符:定义"小于"逻辑,用于大顶堆的默认比较{return (_year < d._year) ||(_year == d._year && _month < d._month) ||(_year == d._year && _month == d._month && _day < d._day); //返回 true 表示当前对象优先级低于参数对象}//3.实现:“>运算符重载函数”bool operator>(const Date& d) const //重载 > 运算符:定义"大于"逻辑,用于小顶堆的 greater<Date> 比较器{return (_year > d._year) ||(_year == d._year && _month > d._month) ||(_year == d._year && _month == d._month && _day > d._day); //返回 true 表示当前对象优先级高于参数对象}private:int _year;int _month;int _day;
};/*-------------------------测试 priority_queue 对自定义类型的使用-------------------------*/
int main()
{/*------------------第一部分:大顶堆示例------------------*//* 注意事项:* 默认使用 vector<Date> 作为底层容器,less<Date> 作为比较器* 依赖 Date::operator< 实现大顶堆(日期大的优先级高)***///1.使用默认的方式定义优先队列 ---> 大顶堆priority_queue<Date> q1;//2.为优先队列赋值 ---> 插入三个日期,按 operator< 排序后,最大的日期在堆顶q1.push(Date(2025, 5, 29));q1.push(Date(2025, 5, 28));q1.push(Date(2025, 5, 30));//3.输出优先队列的队头的元素 ---> 输出堆顶元素(最大日期)cout << q1.top() << endl; /*------------------第二部分:小顶堆示例------------------*//* 注意事项:* 显式指定底层容器为 vector<Date>,比较器为 greater<Date>* 依赖 Date::operator> 实现小顶堆(日期小的优先级高)***///2.显示指定“底层容器 + 比较器”定义优先队列 ---> 小顶堆priority_queue<Date, vector<Date>, greater<Date>> q2;//3.为优先队列赋值 ---> 插入相同的三个日期,按 operator> 排序后,最小的日期在堆顶q2.push(Date(2025, 5, 29));q2.push(Date(2025, 5, 28));q2.push(Date(2025, 5, 30));//3.输出优先队列的队头的元素 ---> 输出堆顶元素(最小日期)cout << q2.top() << endl; return 0;
}
------------模拟实现展示------------
头文件:Stack.h
#pragma once//任务1:包含需要使用的头文件
#include <deque> // 包含双端队列容器,用于作为stack的底层实现//任务2:定义自定义命名空间mySpace //任务2.1:实现:“stack类模板”namespace mySpace
{/** 模板类stack:实现适配器模式的栈容器** 特点:默认使用deque作为底层容器,支持自定义容器类型* 模板参数:* T - 栈中存储的元素类型* Container - 底层容器类型,默认为std::deque<T>*/template<class T, class Container = std::deque<T>> //注意:这里的也传vector<T>class stack{private:Container _con; //支持自定义容器类型,需满足容器适配器的接口要求public:/*---------------------------成员函数---------------------------*//*------------核心接口:栈的基本操作------------*///1.实现:“入栈操作”void push(const T& x){_con.push_back(x); //调用底层容器的push_back()方法}//2.实现:“出栈操作”void pop(){_con.pop_back(); //调用底层容器的pop_back()方法}//3.实现:“获取栈顶元素的操作”const T& top()const{return _con.back(); //调用底层容器的back()方法}/*------------辅助接口:------------*///4.实现:“获取栈中的元素的个数的操作”size_t size()const{return _con.size(); //调用底层容器的size()方法}//5.实现:“判断栈是否为空的操作”bool empty()const{return _con.empty(); //调用底层容器的empty()方法}};// ====================================================查询栈状态=============// 设计说明:// 1. 适配器模式:stack 不直接实现数据结构,而是通过封装其他容器(如:deque、vector)实现功能// 2. 容器选择:默认使用 deque 的原因:// - deque 在尾部操作(push_back/pop_back)均为 O(1) 时间复杂度// - deque 无需预分配额外空间(相比 vector 的扩容机制),内存使用更灵活// 3. 自定义容器:用户可通过模板参数指定其他容器,例如:// stack<int, vector<int>> st; // 使用 vector 作为底层容器(需确保容器支持 back() 和 pop_back())// 4. 容器要求:底层容器必须提供以下接口:// - push_back(const T&):尾部插入元素// - pop_back():尾部删除元素// - back() const:获取尾部元素(栈顶)// - size() const:获取元素个数// - empty() const:判断是否为空// =================================================================
}
头文件:Queue.h
#pragma once//任务1:包含需要使用的头文件
#include <deque> //包含双端队列容器,用于作为queue的底层实现//任务2:定义自定义命名空间mySpace //任务2.1:实现:“quue类模板”namespace mySpace
{template<class T,class Container = std::deque<T>>class queue{private:Container _con; //存储队列元素的底层容器,默认为 deque<T>public:/*---------------------------成员函数---------------------------*//*------------核心接口:队列的基本操作------------*///1.实现:“入队操作”void push(const T& x){_con.push_back(x);}//2.实现:“出队操作”void pop(){_con.pop_back();}//3.实现:“获取队头元素的操作”const T& front()const{return _con.front();}//4.实现:“获取队尾元素的操作”const T& back()const{return _con.back();}/*------------辅助接口:查询队列状态------------*///5.实现:“获取队列中元素的数量的操作”size_t size()const{return _con.size();}//6.实现:“判断队列是否为空的操作”bool empty()const{return _con.empty();}};// =================================================================// 设计说明:// 1. 适配器模式:queue 不直接实现数据结构,而是通过封装其他容器(如:deque、list)实现功能// 2. 容器选择:默认使用 deque 的原因:// - deque 在头部和尾部操作均为 O(1) 时间复杂度// - deque 相比 list 内存更紧凑,访问效率更高// 3. 自定义容器:用户可通过模板参数指定其他容器,例如:// queue<int, list<int>> q; // 使用 list 作为底层容器(需确保容器支持 front() 和 pop_front())// =================================================================
}
头文件:PriorityQueue.h
#pragma once//任务1:包含需要使用的头文件
#include <vector> //包含vector容器,作为优先队列的默认底层存储结构 //任务2:实现比较仿函数
//1.实现:“小于”(用于大堆构建,默认情况)
template<class T>
class Less
{
public://重载()运算符:判断x是否小于y(用于大堆的向上/向下调整)bool operator()(const T& x, const T& y){return x < y; //大堆中,若父节点小于子节点则需要调整}
};//2.实现:“大于”(用于小堆构建)
template<class T>
class Greater
{
public://重载()运算符:判断x是否大于y(用于小堆的向上/向下调整)bool operator()(const T& x, const T& y){return x > y; //小堆中,若父节点大于子节点则需要调整}
};//任务3:定义自定义命名空间mySpace //任务2.1:实现:“priority_queue类模板”namespace mySpace
{//class T:堆中存储的元素类型//class Container:底层容器类型(需支持随机访问和尾部操作)//class Compare:比较仿函数(默认大堆:Less<T>)template<class T, class Container = std::vector<T>, class Compare = Greater<T> >class priority_queue{private:Container _con; //底层容器:存储堆元素,需支持随机访问和尾部操作public:/*------------核心算法:向上/相下 调整算法------------*/////1.实现:“向上调整算法”工具函数//void AdjustUp(HPSortType* a, int child)//{// //1.计算出父节点在数组中的下标(我们有孩子 ---> 得到双亲)// int parent = child - 1 >> 1;// //2.接下来不断的进行向上调整,何时调整结束? ---> 回答:1.当孩子已经调整成根节点时 2.当孩子不满小堆的性质时(这里我们模拟小根堆)// while (child > 0)// {// //3.使用if-else语句进行小根堆的条件检查(小根堆:谁小谁当爹)// if (a[child] >= a[parent]) return;// else// {// //4.1:交换孩子节点和双亲节点的值// Swap(&a[child], &a[parent]);// //4.2:更新孩子的索引 ---> 因为我们要为孩子找到一个合适的位置// child = parent;// //4.3:求出现在孩子的双亲节点的索引// parent = child - 1 >> 1;// }// }//}////2.实现:“向下调整算法”工具函数//void AdjustDown(HPSortType* a, int parent, int n)//{// //1.计算出孩子节点在数组中的下标(我们有双亲节点 ---> 得到“值最小的”孩子)// //这里和向上调整有点不一样:找双亲节点就只有一个,但是找孩子节点有两个,要找哪一个呢?---> 找值最小的孩子// //注:这里使用假设法:假设双亲的左孩子是最小的孩子// int minchild = (parent << 1) + 1;// //2.接下来不断地进行向下调整,何时调整结束 ---> 1.当孩子节点的索引已经大于数组的容量,即:孩子不存在时 2.当孩子不满足小根堆的条件检查时// while (minchild < n)// {// //3.调整出minchild代表是最小的孩子// if (minchild + 1 < n && a[minchild + 1] < a[minchild])// {// minchild++; //切换到右孩子// }// //3.使用if-else语句进行小根堆的条件检查// if (a[minchild] >= a[parent]) return;// else// {// //4.1:交换孩子节点和双亲节点的值// Swap(&a[minchild], &a[parent]);// //4.1:更新双亲节点的索引// parent = minchild;// //4.2:求出现在双亲节点的孩子节点的索引// minchild = (parent << 1) + 1;// }// }//}//1.实现:“向上调整算法”工具函数void AdjustUp(int child) //注意1:void AdjustUp(HPSortType* a, int child) ---> void AdjustUp(int child){//1.Compare com; //注意2:创建仿函数对象 //2.int parent = (child - 1) / 2;//3.while (child > 0){//4.//if (com(_con[child], _con[parent])) return; //非常注意3:if (a[child] >= a[parent]) ---> if (com(_con[parent], _con[child])))if (com(_con[parent], _con[child])) return; else{//5.1:std::swap(_con[child], _con[parent]); //注意4:Swap(&a[child], &a[parent]); ---> std::swap(_con[child], _con[parent]);//5.2:child = parent;//5.3:parent = (child - 1) / 2;}}}//2.实现:“向下调整算法”工具函数void AdjustDown(int parent) //注意1:void AdjustDown(HPSortType* a, int parent, int n) ---> void AdjustDown(int parent){//1.Compare com; //注意2:创建仿函数对象 //2.int minchild = parent * 2 + 1;//3.while (minchild < _con.size()) //注意3:while (minchild < n) ---> while (minchild < _con.size()){//4.if (minchild + 1 < _con.size() && com(_con[minchild + 1] , _con[minchild])) {//注意4:if (minchild + 1 < n && a[minchild + 1] < a[minchild]) ---> if (minchild + 1 < _con.size() && com(_con[minchild + 1] , _con[minchild])) minchild++; }//5.//if (com(_con[minchild] , _con[parent])) return; //非常注意5:if (a[minchild] >= a[parent]) return; ---> if (com(_con[parent], _con[minchild]))if (com(_con[parent], _con[minchild])) return;else{//5.1:std::swap(_con[minchild], _con[parent]); //注意6:Swap(&a[minchild], &a[parent]); ---> std::swap(_con[minchild], _con[parent]);//5.1:parent = minchild;//5.2:minchild = parent * 2 + 1;}}}/*------------核心接口:优先队列的基本操作------------*///1.实现“优先队列的入队操作”void push(const T& x){//1.先将元素添加到容器的尾部_con.push_back(x);//2.再从新元素位置开始向上调整堆AdjustUp(_con.size() - 1);}//2.实现:“优先队列的出队操作”void pop(){//1.先判断优先队列是否为空if (empty()) return;//2.然后让堆顶元素和最后一个元素进行交换std::swap(_con[0], _con[_con.size() - 1]);//3.接着删除尾部元素_con.pop_back();//4.最后从堆顶开始向下调整堆AdjustDown(0);}//3.实现:“获取堆顶元素的操作”const T& top(){return _con[0];}/*------------辅助接口:查询优先队列状态------------*///1.实现:“获取优先队列中的元素的数量的操作”size_t size()const{return _con.size();}//2.实现:“判断优先队列是否为空的操作”bool empty()const{return _con.empty();}};// =================================================================// 设计说明:// 1. 适配器模式:priority_queue 不直接实现堆结构,而是通过封装容器(如:vector)实现功能// 2. 容器选择:默认使用 vector 的原因:// - vector 支持随机访问(通过下标快速定位父子节点)// - push_back()/pop_back() 效率高(均摊 O(1),配合堆调整实现整体高效)// 3. 比较策略:通过仿函数 Compare 灵活切换大堆/小堆:// - 默认 Compare=Less<T> 实现大堆(堆顶为最大值)// - 若需小堆,传入 Compare=Greater<T>(堆顶为最小值)// 4. 自定义容器:用户可指定其他支持随机访问的容器,例如:// priority_queue<int, deque<int>, Greater<int>> pq; // 使用 deque 作为底层容器// 5. 容器要求:底层容器必须提供以下接口:// - operator[]:随机访问元素(用于堆节点定位)// - push_back(const T&):尾部插入元素// - pop_back():尾部删除元素// - size() const:获取元素个数// =================================================================
}
测试文件:Test.cpp
#include<iostream>
#include<vector>
#include<list>
#include<stack>
#include<queue>
#include<algorithm>#include"Stack.h" //自定义栈容器(适配器模式)
#include"Queue.h" //自定义队列容器(适配器模式)
#include"PriorityQueue.h" //自定义优先队列(堆结构)using namespace std;/*===================== 测试栈和队列的基本使用 =====================*/
void test01()
{/*------------------------第一部分:测试栈的功能------------------------*/cout << "------------测试栈的功能------------" << endl;/*------------第一步:创建栈------------*///1.使用vector作为底层容器(默认容器为deque,但显式指定vector测试)//mySpace::stack<int, vector<int>> stk;//使用list作为底层容器mySpace::stack<int, list<int>> stk; //注意:也可使用list作为底层容器(需确保容器支持push_back/pop_back/back)/*------------第二步:测试“入栈操作”------------*/stk.push(1);stk.push(2);stk.push(3);stk.push(4);/*------------第三步:测试“获取栈顶元素的操作”------------*/cout << "入栈之后,此时栈顶的元素是:" << stk.top() << endl;/*------------第四步:测试“出栈操作”------------*/stk.pop();cout << "出栈之后,此时栈顶的元素是:" << stk.top() << endl;/*------------------------第一部分:测试队列的功能------------------------*/cout << "------------测试队列的功能------------" << endl;/*------------第一步:创建队列------------*///1.使用deque作为底层的容器(默认使用deque作为底层容器)mySpace::queue<int> que;//2.显式指定list作为底层的容器(需支持push_back/pop_front/front/back)//mySpace::queue<int, list<int>> que;/*------------第二步:测试“入队操作”------------*/que.push(1);que.push(2);que.push(3);que.push(4);cout << "入队之后,此时队头的元素是:"<< que.front() << endl;cout << "入队之后,此时队尾的元素是:"<< que.back() << endl;// 出队:移除队头元素1que.pop();cout << "出队之后,此时队头的元素是:" << que.front() << endl;cout << "出队之后,此时队尾的元素是:" << que.back() << endl;
}/*===================== 测试优先队列(堆)的功能 =====================*/void test02()
{cout << "------------测试优先队列(堆)的功能------------" << endl;/*------------第一步:创建优先队列------------*///1.不指定仿函数,使用默认的仿函数Less<int>(大堆,堆顶为最大值)//mySpace::priority_queue<int> pqu;//2.指定仿函数Greater<int>(小堆,堆顶为最小值)mySpace::priority_queue<int, vector<int>, Greater<int>> pqu; //实例化小堆:通过Greater仿函数指定“x > y”为真时交换(堆顶为最小值)/*------------第二步:测试“入堆操作”------------*/pqu.push(4);pqu.push(1);pqu.push(5);pqu.push(7);pqu.push(9);/*------------第四步:测试“出堆操作”------------*/while (!pqu.empty()) //按优先级顺序输出元素(小堆顺序:1 4 5 7 9){cout << pqu.top() << " "; //输出堆顶元素pqu.pop(); //移除堆顶元素并调整堆}cout << endl;
}int main()
{test01();test02();return 0;
}
运行结果:
------------代码片段剖析------------
上面的代码逻辑相对之前容器的逻辑较为简单,但容器适配器
priority_queue
的实现相对会复杂一点,原因在于它结合了比较器
和堆结构
- 堆结构引入了向上调整算法和向下调整算法,而由于需要适配仿函数(灵活切换大堆 / 小堆的比较规则),这两种算法的逻辑需要与仿函数联动。
因此:下面的代码片段都是讲解这两个调整算法为什么要进行那样的修改的
(注:关于
if
语句中如何通过仿函数调整比较逻辑,可参考下面关于仿函数的专门讲解)
片段一:AdjustUp和AdjustDown方法的参数为什么进行了简化?
//1.实现:“向上调整算法”工具函数
void AdjustUp(int child) //注意1:void AdjustUp(HPSortType* a, int child) ---> void AdjustUp(int child)//2.实现:“向下调整算法”工具函数
void AdjustDown(int parent) //注意1:void AdjustDown(HPSortType* a, int parent, int n) ---> void AdjustDown(int parent)
在自定义的
priority_queue
类模板中,AdjustUp
和AdjustDown
方法的参数之所以从原始的数组指针和长度简化为child
或parent
下标是因为:类模板内部已经封装了底层容器(如:
vector
),无需再通过外部传入数据数组和长度
一、原始参数设计的问题(注释中的旧版本)
主要问题:
- 这种设计需要用户手动传入数组和长度,耦合性高且不通用。
- 当
priority_queue
封装了底层容器(如:vector
)后,这些数据应属于类的内部状态,无需外部暴露。
二、修改后参数简化的原因(当前版本)
核心改进:
- 封装底层容器:
- 类模板中定义了成员变量
Container _con
(如:vector<T>
),用于存储堆元素。AdjustUp
和AdjustDown
直接通过_con
访问元素,无需外部传入数组指针。- 自动获取容器长度:
- 通过
_con.size()
获取当前元素个数,替代旧版本的n
参数,避免手动传递长度可能导致的越界问题。- 依赖类模板参数:
- 比较逻辑通过类模板参数
Compare
实现(仿函数com
),而非硬编码比较规则,提升了代码的灵活性(可适配大堆 / 小堆)。
片段二:Swap(&a[child], &a[parent])的传参为什么发生了改变?
//5.1:
std::swap(_con[child], _con[parent]); //注意4:Swap(&a[child], &a[parent]); ---> std::swap(_con[child], _con[parent]);//5.1:
std::swap(_con[minchild], _con[parent]); //注意6:Swap(&a[minchild], &a[parent]); ---> std::swap(_con[minchild], _con[parent]);
在代码中使用
std::swap(_con[child], _con[parent])
替换原始的Swap(&a[child], &a[parent])
主要是因为:底层容器的封装方式改变 和 C++ 标准库的易用性。
一、原始代码的问题(注释中的旧版本)
a
是外部传入的数组指针(如:HPSortType* a
),需要通过&a[child]
获取元素地址,再传递给自定义的Swap
函数进行交换。- 这种方式依赖原始数组的内存布局,且
Swap
函数可能需要手动实现(如:通过指针交换值),耦合性高且不通用。
二、修改后使用 std::swap 的原因(当前版本)
- 自定义的
priority_queue
类模板中,元素存储在成员变量_con
(底层容器,如:vector<T>
)中。_con[child]
和_con[parent]
直接通过容器的operator[]
接口访问元素(类似数组下标访问),无需通过指针操作原始内存。
------------核心问题深究------------
一、仿函数
1. 什么是仿函数?
仿函数(Functor)
:也称为函数对象(Function Object),是一种在 C++ 中通过类或结构体实现的可调用实体
- 仿函数的核心特点是 重载了 operator() 运算符,使得类的对象可以像普通函数一样被调用。
- 仿函数是 C++ 中
“以类模拟函数”
的重要机制,这种机制结合了面向对象的封装性
和函数的灵活性
,在 STL和算法中有着广泛应用。
仿函数的核心特点:
1. 重载 operator()
通过在类中定义
operator()
运算符,使得该类的对象可以像函数一样被调用:class Add { public:int operator()(int a, int b) // 重载()运算符{ return a + b;} };
使用时:
Add add; //创建仿函数对象 int result = add(3, 5); //像函数一样调用,结果为8
2. 可携带状态
- 仿函数可以像普通类一样拥有成员变量,用于存储
状态
或参数
,这使得它比普通函数更灵活。
class Multiply
{
private:int factor; // 成员变量存储状态
public:Multiply(int f) // 构造函数初始化状态:factor(f) {} int operator()(int x) {return x * factor;}
};Multiply mul(3); // 创建一个乘以3的仿函数对象
cout << mul(4); // 输出12(携带状态factor=3)
2. 仿函数有什么用途?
1. STL算法中的比较器和谓词
比较器:通过仿函数指定排序规则(如:升序 / 降序)
#include <algorithm> #include <vector> using namespace std;class Greater { public:bool operator()(int a, int b) const // 降序比较{return a > b; } };int main() {vector<int> nums = {3, 1, 4, 2};sort(nums.begin(), nums.end(), Greater()); // 使用仿函数指定降序// 排序结果:4 3 2 1return 0; }
谓词:用于筛选元素(如:
find_if
、remove_if
)class IsEven { public:bool operator()(int x) const {return x % 2 == 0; // 判断是否为偶数} };vector<int>::iterator it = find_if(nums.begin(), nums.end(), IsEven());
2. 自定义算法逻辑
在自定义算法中,通过仿函数将逻辑抽象出来,提高代码复用性。
template<class Compare> void BubbleSort(int* a, int n, Compare com) {// 使用com(a[j], a[j - 1])进行比较,实现通用排序if (com(a[j], a[j - 1])) // 根据仿函数的逻辑决定是否交换{ swap(a[j-1], a[j]);} }
3. 替代函数指针
- 相比函数指针,仿函数
更安全
(类型检查严格)、更灵活
(可携带状态),且性能接近函数调用
(编译器易优化)
3. 仿函数与普通函数的区别有哪些?
特性 | 普通函数 | 仿函数 |
---|---|---|
调用方式 | 直接调用(如:func(a, b) ) | 通过对象调用(如:obj(a, b) ) |
状态存储 | 无法存储状态 | 可通过成员变量存储状态 |
类型安全 | 弱(依赖函数指针类型匹配) | 强(编译期模板类型检查) |
可复用性 | 低(逻辑固定) | 高(通过模板和继承扩展) |
重载 | 通过不同参数列表 | 通过多个operator()实现 |
性能 | 取决于编译器 | 容易 |
4. 怎么让冒泡排序既可以升序又可以降序排序?
想必大家对冒泡排序已经相当了解了。
假如我们想要对一批数据使用冒泡排序同时实现升序和降序排序,该怎么做呢?
可能有些小伙伴会说:“这很简单的啦 ~,把原来的冒泡排序代码复制一份,修改其中的比较逻辑就行啦 ~!”
哈哈,小伙伴的回答简单粗暴没毛病,但是呢会导致重复代码冗余,违背了DRY(Don’t Repeat Yourself)原则
为解决这个问题,我们可以引入
仿函数(Functor)
来实现比较逻辑的抽象,避免硬编码比较规则,从而提升代码的灵活性和可维护性。
代码案例:使用仿函数使冒泡排序同时实现升序和降序排序
#include <iostream>
using namespace std;/*===================== 仿函数(函数对象)的原理与使用 =====================*/
//说明:仿函数是重载了operator()的类,其对象可像函数一样调用
//应用场景:STL算法(如:sort、priority_queue)中的比较器、谓词等/*---------------------------第一部分:实现比较仿函数(类模板)---------------------------*/
template<class T>
class Less
{
public:bool operator()(const T& x, const T& y) const //注意:加const提高通用性{return x < y; //返回true时表示x应排在y前面(升序)}
};template<class T>
class Greater
{
public:bool operator()(const T& x, const T& y) const{return x > y; //返回true时表示x应排在y后面(降序)}
};/*---------------------------第二部分:实现冒泡排序(函数模板)---------------------------*/
template<class Compare>
void BubbleSort(int* a, int n, Compare com)
{//1.外层的循环控制 ---> 记录需要进行交换的数字的个数[“也可理解为冒泡的总的趟数”](解释:n个数字只需要对n-1个数字进行排序即可实现n个数字的有序)for (int i = 1; i <= n - 1; i++){//2.内层的循环控制 ---> 记录对每个数字进行排序需要交换的次数[“也可以理解为每趟冒泡需要交换的次数”](注意:每趟冒泡需要交换的次数是不同的)//对冒泡排序进行优化:int flag = 1;for (int j = 1; j <= n - i; j++){//核心:前面的元素的值大进行交换//if (a[j - 1] > a[j])//if (com(a[j - 1], a[j]))if (com(a[j], a[j - 1])){swap(a[j - 1], a[j]);flag = 0;}/*下面的注释的内容很重要!!!注意:大家很明显可以看出来这个“冒泡排序”是我直接复制在数据结构初阶中讲解“八大排序”时冒泡排序的代码哈哈,如果你也是像我一样直接进行的复制后,仅仅将“if (a[j - 1] > a[j])”修改成“if (com(a[j - 1], a[j]))”的话,你运行出来的结果是:使用“仿函数Less<int> lessFunc”进行升序排序:9 8 7 6 5 4 3 2 1使用“仿函数Greater<int>”进行降序排序:1 2 3 4 5 6 7 8 9使用“匿名对象Less<int>()”进行升序排序:9 8 7 6 5 4 3 2 1使用“匿名对象Greater<int>()”进行降序排序:1 2 3 4 5 6 7 8 9我们发现我们期望的“升序/降序”和实际的“升序/降序”正好相反也就是说:为了适应仿函数,使用Less<int>排升序,使用Greater<int>排降序BubbleSort中涉及比较的的部分,如:“if (a[j - 1] > a[j])”除了要将其修改为“if (com(a[j - 1], a[j]))”还要注意:com(参数1,参数2)需要满足“参数1<参数2” 举个例子:就是上面的if条件判断是:“if (a[j - 1] > a[j])”,为了使用仿函数,也就是说所有的条件判断我只能使用“<”所以我就将上面的if条件判断等价的修改成了“if (a[j] < a[j - 1])”接着就可以直接将“if语句”转换为“仿函数对象的调用”“if (a[j] < a[j - 1])”----> “com (a[j] , a[j - 1])”*/}if (flag) break; //如果某一趟的冒没有进行交换,说明当前数组中的元素已经有序了,则直接退出}
}int main()
{/*------------第一部分:创建仿函数对象------------*/Less<int> lessFunc; //实例化仿函数对象Greater<int> greaterFunc;/*------------第二部分:展示两种调用仿函数的方式------------*///第一种:对象()cout << lessFunc(1, 2) << endl; //输出1(true的整数表示为1)//第二种:operator()cout << lessFunc.operator()(1, 2) << endl; //等价调用/*------------第三部分:冒泡排序结合仿函数进行排序------------*/int a[] = { 9,1,2,8,5,7,4,6,3 };int n = sizeof(a) / sizeof(a[0]);//1.使用仿函数进行冒泡排序(升序/降序)cout << "使用“仿函数Less<int> lessFunc”进行升序排序:" << endl;BubbleSort(a, n, lessFunc); //升序排序for (int i = 0; i < n; i++){cout << a[i] << " ";}cout << endl;cout << "使用“仿函数Greater<int>”进行降序排序:" << endl;BubbleSort(a, n, greaterFunc); //降序排序for (int i = 0; i < n; i++){cout << a[i] << " ";}cout << endl;//2.也可直接传递匿名对象(临时对象)cout << "使用“匿名对象Less<int>()”进行升序排序:" << endl;BubbleSort(a, n, Less<int>());for (int i = 0; i < n; i++){cout << a[i] << " ";}cout << endl;cout << "使用“匿名对象Greater<int>()”进行降序排序:" << endl;BubbleSort(a, n, Greater<int>());for (int i = 0; i < n; i++){cout << a[i] << " ";}cout << endl;return 0;
}
二、容器适配器
1. 什么是容器适配器?
容器适配器(Container Adapter)
:是 C++ 标准库中的一种特殊容器,它不直接提供完整的数据存储功能,而是通过封装其他底层容器(如:vector
、deque
、list
)并限制其接口,来实现特定的数据结构。
- 这种设计遵循适配器模式,将已有容器的功能转换为用户期望的接口。
- 它们基于现有的序列容器(如:vector、list、deque)提供特定的接口和行为,实现了不同的数据结构抽象。
核心特点:
封装底层容器
:适配器内部维护一个底层容器对象(如:deque
),所有操作都委托给该容器执行限制接口
:适配器仅暴露特定的接口(如:栈的push/pop/top
),隐藏底层容器的其他功能,确保数据结构的语义正确性可配置底层容器
:用户可通过模板参数指定底层容器类型,默认使用deque
(兼顾效率与灵活性)
2. C++中的三种标准容器适配器的对比差别?
适配器 | 默认底层容器 | 可选底层容器 | 数据结构 | 主要操作 | 典型用途 |
---|---|---|---|---|---|
stack | deque | vector list | 栈 (LIFO) | push, pop top | 函数调用栈 撤销操作 |
queue | deque | list | 队列 (FIFO) | push, pop front, back | 任务调度 消息队列 |
priority_queue | vector | deque | 优先队列 | push, pop top | 任务优先级调度 Dijkstra算法 |
为什么queue的底层容器不可以是:vector?
适配器queue
:底层容器既可以是标准容器类模板(如:deque
、list
等),也能是其他专门设计的容器类。但这类底层容器至少需支持以下操作:
front
:返回队头元素的引用(对应容器头部元素访问)back
:返回队尾元素的引用(对应容器尾部元素访问)pop_front
:在容器头部执行操作,实现队列 “头部出队” 逻辑push_back
:在容器尾部执行操作,实现队列 “尾部入队” 逻辑size
:返回容器中有效元素的个数(即队列有效元素数量)empty
:检测容器(对应队列逻辑)是否为空
在 C++ 标准库中,deque 和 list 这两种标准容器类满足上述全部要求,而容器vector却不满足上面的要求
- 因此,默认情况下,若创建
queue
时未手动指定底层容器,STL 会自动使用deque
作为其底层容器。- 也可以显示指定使用
list
作为底层容器。- 但是不能使用
vector
作为底层容器。
为什么priority_queue的底层容器不可以是:list?
适配器priority_queue
:底层容器既可以是标准容器类模板(如:vector
、deque
等),也可以是其他专门设计的容器类。但该容器至少需支持以下操作:
- 支持随机访问迭代器,以便内部能通过迭代器操作维持堆结构
- 需实现以下基础操作:
front()
:返回容器中第一个元素的引用(对应堆顶逻辑)push_back()
:在容器尾部插入元素(用于新元素入堆)pop_back()
:删除容器尾部元素(用于堆顶元素出堆后调整结构 )size()
:返回容器中有效元素的个数empty()
:检测容器是否为空
在 C++ 标准库中,vector 和 deque 这两种标准容器类满足上述全部要求,而容器list 却不满足上面的要求
- 因此,默认情况下,若创建
priority_queue
时未手动指定底层容器,STL 会自动使用vector
作为其底层容器。- 也可以显示指定使用
deque
作为底层容器。- 但是不能使用
list
作为底层容器。
为什么priority_queue的默认的底层容器是:vector?
看了上面的分析,相信大家已经知道为什么容器适配器priority_queue的底层容器为什么不能是list了
但是我相信一定还有小伙伴会疑惑:“既然 vector和deque都可以作为priority_queue的底层容器,那为什么默认选择vector而不是deque呢?”
这主要与 priority_queue 的
核心操作特性
和两种容器的性能差异
有关
具体原因如下:
priority_queue
的核心操作(如:push
、pop
)依赖 随机访问迭代器 和 下标操作 来调整堆结构(调用std::push_heap
、std::pop_heap
等算法)
vector的优势:
支持连续内存存储和随机访问迭代器
通过下标
operator[]
访问元素的时间复杂度为O(1)O(1)O(1)deque的劣势:
虽然也支持随机访问迭代器,但内部采用分段连续存储(缓冲区链表),迭代器的底层实现更复杂
下标访问的性能略低于
vector
口说无凭,接下来我们通过一个测试案例来直观对比
vector
与deque
两个容器的随机访问性能差异。代码案例:性能测试vector与deque的排序效率对比
#include <iostream>
#include <vector>
#include <deque>
#include <algorithm>
using namespace std;/*===================== 性能测试:vector与deque的排序效率对比 =====================*/
//测试目的:比较vector和deque在排序操作中的性能差异
//结论:vector排序效率更高,因内存连续,CPU缓存利用率更好void test_op1()
{cout << "===========性能测试:vector与deque的排序效率对比===========" << endl;/*----------------第一阶段:设置随机环境----------------*///1.种随机数种子srand(unsigned int(time(0)));//2.设随机数据量const int N = 1'000'000; //测试数据量:100万/*----------------第二阶段:定义vector和deque容器----------------*/vector<int> vec; //动态数组(连续内存)deque<int> deq; //双端队列(非连续内存)/*----------------第三阶段:赋值vector和deque容器相同的随机数----------------*/for (int i = 0; i < N; ++i) {auto e = rand() + i; vec.push_back(e);deq.push_back(e);}/*----------------第四阶段:测试vector和deque容器排序时间----------------*///1.测试vector排序时间int begin1 = clock(); sort(vec.begin(), vec.end()); //使用STL排序算法(基于快速排序/归并排序)int end1 = clock(); //2.测试deque排序时间int begin2 = clock();sort(deq.begin(), deq.end()); //deque迭代器为双向迭代器,排序效率低于vectorint end2 = clock();/*----------------第五阶段:输出vector和deque容器排序时间----------------*/// 输出耗时(单位:毫秒,clock()/CLOCKS_PER_SEC=秒)printf("vector排序耗时:%d ms\n", end1 - begin1);printf("deque排序耗时:%d ms\n", end2 - begin2);
}/*===================== 性能优化测试:deque排序的优化方案 =====================*/
//测试目的:验证将deque数据拷贝到vector后排序是否更高效
//结论:拷贝后排序+拷贝回deque的总耗时 低于 直接排序dequevoid test_op2()
{cout << "===========性能优化测试:deque排序的优化方案===========" << endl;/*----------------第一阶段:设置随机环境----------------*/srand(unsigned int(time(0)));const int N = 1000000;/*----------------第二阶段:定义两个deque容器----------------*/deque<int> deq1; //原始dequedeque<int> deq2; //用于测试优化方案的deque/*----------------第三阶段:赋值两个deque容器相同的随机数----------------*/for (int i = 0; i < N; ++i) {auto e = rand() + i;deq1.push_back(e);deq2.push_back(e);}/*----------------第四阶段:测试两个deque容器排序时间----------------*///1.测试直接排序deque的耗时int begin1 = clock();sort(deq1.begin(), deq1.end()); //直接排序deque(双向迭代器,非连续内存)int end1 = clock();//2.测试优化方案:拷贝到vector排序后再拷贝回dequeint begin2 = clock();vector<int> v(deq2.begin(), deq2.end()); // 拷贝deque数据到vector(连续内存)sort(v.begin(), v.end()); // 高效排序vectordeq2.assign(v.begin(), v.end()); // 拷贝回dequeint end2 = clock();/*----------------第五阶段:输出两个deque容器的排序时间----------------*/printf("deque直接排序耗时:%d ms\n", end1 - begin1);printf("deque拷贝到vector排序后拷贝回耗时:%d ms\n", end2 - begin2);
}int main()
{test_op1();test_op2(); return 0;
}
3. 使用容器适配器需要注意那些事情?
使用容器适配器时我们需要注意以下两点的内容:
迭代器限制
:适配器不提供迭代器
- 例如:
begin()
、end()
,因为其设计目的是限制访问方式(如:栈只能访问栈顶)
底层容器选择
:根据操作频率选择容器
- 例如:
stack
频繁push/pop
时选deque
,需随机访问选vector
三、序列容器:deque(双端队列)
相信大家看了上面的关于容器适配器的介绍,已经对于这个底层容器deque有了一定的了解
虽然我们之前解释了为什么
priority_queue
的默认底层容器是vector
,但可能有些小伙伴还未完全理解其中的逻辑。此外,或许还有小伙伴想知道另一个问题的答案:为什么 STL 选择
deque
作为stack
和queue
的底层默认容器?那下面,就让我们深入探究这个容器的特性,解开这些疑惑吧!
1. 什么是deque?
deque(双端队列)
:是 C++ 标准模板库(STL)中的一种序列式容器,全称是double-ended queue
它允许在容器的头部和尾部高效地执行元素的插入、删除和访问操作
同时支持随机访问(通过下标访问元素)
总结:它结合了 vector 和 list 的部分优点。
deque的核心特点:
1. 双端操作高效
- 可以在 O(1)O (1)O(1) 时间复杂度内从头部(
front
)或尾部(back
)插入 / 删除元素
- 性能与
list
相当,但比vector
更灵活(vector
仅尾部操作高效)
2. 随机访问支持
- 提供随机访问迭代器(
operator[]
和at()
方法),可以像数组一样通过下标直接访问元素,时间复杂度为 O(1)O(1)O(1)
- 但与
vector
不同,deque
的内存并非连续存储,因此迭代器的底层实现更复杂,随机访问的缓存效率略低于vector
3. 动态扩容
- 无需预先指定容量,可根据元素数量动态扩展
- 扩容时通常不会像
vector
一样整体复制元素,而是通过增加分段连续的内存块(缓冲区)来扩展,避免了大规模数据移动
2. deque的底层结构是什么样的?
物理存储结构
deque
的底层实现通常通过分块数组(分段连续存储)
实现:
- 由多个固定大小的数组块(buffer)组成,每个块存储部分元素。
- 通过一个中央控制映射表(map)管理,记录每个缓冲区的地址和边界,保证逻辑上的连续性。
总结: 这种结构使得 deque 在头部和尾部扩展时只需分配新的缓冲区,无需移动已有元素,适合需要频繁双端操作的场景。
迭代器的设计
deque(双端队列)的底层并非真正的连续内存空间,而是由多个分段连续的小内存块(缓冲区)拼接而成。
- 从逻辑上看,它类似于一个动态的二维数组,通过巧妙的设计让用户感知到 “整体连续” 的访问体验,但实际上每个小段内存是连续的,段与段之间通过指针连接。
为了维护这种连续空间和随机访问的假象,deque 的迭代器承担了关键角色。
由于其内存结构的特殊性,
deque
的迭代器需要处理跨段访问的逻辑,因此设计比vector
的迭代器更复杂。具体来说,迭代器需要记录:
当前所在的缓冲区
缓冲区中的位置
以及指向下一个 / 上一个缓冲区的指针
从而在遍历或随机访问时能够正确跳转至目标位置。
简单来说:
deque
通过 分段连续存储 + 复杂迭代器 的组合,实现了双端高效操作与随机访问的平衡,尽管底层物理空间不连续,但逻辑上呈现为一个连续的队列
3. deque的优势是什么?
当我们了解了上面
deque
的 物理存储结构(分段连续内存块)和迭代器的设计(跨段访问逻辑) 后,下面关于deque
与vector
的对比就不难理解了。
特性 | vector | deque |
---|---|---|
内存连续性 | 连续存储 | 分段连续存储(非连续) |
随机访问性能 | 高(缓存利用率好) | 稍低(跨缓冲区时需指针跳转) |
头部操作效率 | O (n)(需移动所有元素) | O(1) |
扩容策略 | 按需翻倍(整体复制) | 新增缓冲区(局部扩展) |
适用场景 | 随机访问多、尾部操作多 | 双端操作多、长度频繁变化 |
deque的优势在于:
与vector相比:
头部操作高效
:在头部插入或删除元素时,无需像vector
那样搬移后续所有元素,时间复杂度为 O(1)O(1)O(1),效率显著更高。扩容代价低
:扩容时,无需像vector
那样整体复制大量元素,只需新增或释放分段连续的小内存块(缓冲区),避免了大规模数据移动,尤其适合频繁动态扩展的场景。与list相比:
deque
的底层采用分段连续空间(而非链表结构),空间利用率更高
,且无需为每个元素存储额外的指针字段。
4. deque的致命缺陷是什么?
在此之前,我们提到过
deque
这种序列容器是vector
和list
“各取所长” 的结合体:它既有vector
随机访问的特性,又有list
双端高效操作的优势。并且在多数场景的性能对比中表现不俗,仅在连续随机访问性能上略逊于
vector
,那么请问:既然 deque 看起来如此优秀,为什么我们在之前的数据结构学习中没有接触过它呢?
哈哈,我们就不得不谈谈它的致命缺陷了!!!
deque 存在一个致命缺陷:不适合频繁遍历
- 由于其底层是分段连续的内存块,遍历时迭代器需要频繁检测是否到达当前缓冲区的边界,并跳转至下一段缓冲区,这会导致额外的性能开销,遍历效率低于
vector
和list
在需要线性遍历的场景中,
deque
的这一特性可能成为瓶颈,因此实际开发中,当需要线性数据结构时,开发者通常优先选择
vector
(适合随机访问
和尾部操作
)list
(适合双向遍历
和中间插入/删除
)
而
deque
的应用场景相对较少,目前,deque
的典型应用是作为 STL 中stack
(栈)和queue
(队列)的底层数据结构,充分利用其双端操作高效的特性,同时规避了频繁遍历的需求。
5. 为什么STL选择deque作为stack和queue的底层默认容器?
stack(栈)
:是一种后进先出(LIFO)的线性数据结构。
因此只要具备
push_back()
和pop_back()
操作的线性容器,都可以作为其底层容器。例如:
vector
和list
+++++++++++++++++++++++++++++++++
queue(队列)
:是一种先进先出(FIFO)的线性数据结构。
只需支持
push_back()
和pop_front()
操作的容器即可作为底层容器。例如:
list
但在 STL 中,
stack
和queue
默认选择deque
作为底层容器,主要原因如下:1. 操作场景匹配:
stack
和queue
不需要遍历元素(因此标准库中它们不提供迭代器),仅需在固定一端或两端进行操作
- 如:栈的栈顶、队列的队头和队尾
deque
的双端操作效率为 O(1)O(1)O(1),且无需遍历,完美契合这种场景。
deque
的主要缺陷是遍历效率低(因迭代器需处理跨缓冲区跳转),但stack
和queue
完全不涉及遍历操作,因此能完美避开这一缺陷,充分发挥其双端操作和动态扩容的优势。
2. 性能与内存优势:
- 对于stack:元素增长时,
deque
的扩容无需像vector
那样整体搬移数据,只需新增分段连续的内存块(缓冲区),效率更高- 对于queue:元素增长时,
deque
既能在尾部高效插入(push_back
),又能在头部高效删除(pop_front
),且分段存储的内存利用率高于list
(无需为每个元素存储指针)综上:
deque
的特性与stack
、queue
的操作需求高度契合,成为 STL 选择其作为默认底层容器的核心原因。