文章标题:
C++容器适配器深度剖析:栈、队列、双端队列的底层与应用探究
文章内容
文章目录
- 前言
- stack
- 常用接口概览
- stack的模拟实现
- queue
- 常见接口说明
- queue的模拟实现
- deque
- 关键接口介绍
- 容器适配器
- priority_queue
- 常用接口解读
- priority_queue模拟实现
- 反向迭代器的实现剖析
- 仿函数(函数对象)
- 作业板块
- 逆波兰表达式引申
- 拓展内容
前言
在C++标准库的宏大架构中,数据结构是构建高效编程的重要基础,而容器适配器、序列容器以及相关算法逻辑,是其中极具实用价值的核心组成部分。无论是日常开发还是算法刷题,栈(stack)、队列(queue)、优先级队列(priority_queue)这类“常客”频繁出现,它们看似简单的接口背后,暗藏着对数据存取规则的精妙设计——栈的“后进先出”适配递归调用、括号匹配等场景,队列的“先进先出”适配层序遍历、任务调度等需求,优先级队列通过堆结构实现带权重的数据筛选,成为解决TopK问题的有效手段。
深入探究这些结构时,我们会产生诸多疑问:为何栈和队列默认的底层容器是deque而非vector或list?deque的“分段连续”存储有何独特之处,能兼顾头尾操作效率与一定的随机访问能力?优先级队列中,仿函数(函数对象)如何灵活把控堆的排序逻辑?反向迭代器的设计又暗藏哪些技巧,能实现遍历方向反转却不影响底层数据结构?这些问题的答案,正是从“会用”迈向“精通”的关键所在。
本文不仅会系统梳理栈、队列、deque、优先级队列的核心接口与应用场景,还会通过完整的模拟实现代码,剖析容器适配器的封装逻辑——比如栈如何依托deque的尾插尾删实现“后进先出”,优先级队列怎样通过向上/向下调整算法维护堆结构。同时,针对仿函数、反向迭代器等易混淆概念,结合实例解析其原理与应用,助你厘清“less与greater的差别”“反向迭代器的++为何是底层迭代器的--”等细节。
理论结合实战更能巩固所学。文中选取力扣(如最小栈、二叉树层序遍历、数组第K大元素)和牛客网(如栈的压入弹出序列)的经典题目,不仅提供解题思路,还附上完整代码实现,让你在刷题过程中直观感受容器的妙用。此外,针对容器特性的选择题解析(如不同容器迭代器的失效问题、deque与vector的区别),可帮你规避学习中的常见误区。
无论你是初涉C++容器的新手,还是想夯实数据结构基础的进阶者,本文都能为你搭建一条从底层原理到实战应用的清晰路径。吃透这些知识,不仅能让你在面对复杂问题时快速找到数据结构选型的最优解,更能培养对代码逻辑的深层理解——毕竟,真正的编程能力,往往源于对“为何如此设计”的追问与探索。
stack
栈遵循后进先出的规则,需注意区分栈顶与栈底,栈顶是最后存入的元素。
常用接口概览
empty():判断栈是否为空。size():获取栈中元素的个数。top():获取栈顶元素。push(val):向栈中压入元素。pop():弹出栈顶元素。
需留意,栈没有迭代器,若访问空栈可能引发未定义行为。栈的构造示例:
stack<int>st;
stack的模拟实现
namespace renshen
{
template<class T, class Container = deque<T>>
class stack
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
T& top()
{
return _con.back();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
//补充:queue和stack库里面也有容器适配器
}
queue
队列遵循先进先出的规则。
常见接口说明
empty():判断队列是否为空。size():获取队列中元素的个数。front():获取队列的队首元素。back():获取队列的队尾元素。push(val):向队列中插入元素。pop():弹出队列的队首元素。
同样,队列没有迭代器。队列的构造示例:
queue<int>a;
queue的模拟实现
namespace renshen
{
template<class T, class Container = deque<T>>
class queue
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_front();
}
T& front()
{
return _con.front();
}
T& back()
{
return _con.back();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
//引申:vector和list都有front和back
}
deque
对比vector:
1. deque极大缓解了扩容问题,且支持头插头删操作。
2. deque的[]操作效率较低,随机访问速度慢。
对比list:
1. deque支持下标随机访问。
2. deque的CPU高速缓存效率较好。
3. list在中间插入方面表现更优。
总结:deque适用于高频的头插头删、尾插尾删操作,同时有少量随机访问需求的场景。deque的模拟实现采用中控指针数组(控制分散的区域,而非单个数据)。
当中控指针数组满时,进行扩容即可。
关于deque的[]模拟写法(如[i]):
1. 首先判断是否在第一个数组范围内,若不在则减去第一个数组的元素个数,进而确定所在数组。
关键接口
begin、end、rbegin、rend、size、empty、resize、[]、front、back、erase、clear、push_front、push_back、pop_front、pop_back、insert
容器适配器
通常以
container命名,使模板具备相应特性。
priority_queue
优先级队列实质是堆(堆顶为最上层元素)。
上述为类模板定义,默认是大堆。使用示例:priority_queue<int,vector<int>,greater<int>> pq
引申:需区分greater和greater (),前者是函数模板,后者是类模板。
此为构造函数的声明。
解决TopK问题时,堆排序较为高效,全排序时sort的时间复杂度为nlogn。
常用接口解读
empty():判断优先级队列是否为空。size():获取优先级队列中元素的个数。top():获取优先级队列的堆顶元素。push(val):插入元素并自动排序。pop():弹出堆顶元素。
优先级队列没有迭代器,遍历需逐个元素访问。
priority_queue模拟实现
namespace renshen
{
template<class T, class Container = vector<T>, class Comapre = less<T>>
class priority_queue
{
private:
void AdjustDown(int parent)
{
Comapre com;
size_t child = parent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.size() && com(_con[child],_con[child + 1]))
{
++child;
}
if (com(_con[parent],_con[child]))
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else break;
}
}
void AdjustUp(int child)
{
Comapre com;
int parent = (child - 1) / 2;
while (child > 0)
{
if (com(_con[parent],_con[child]))
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else break;
}
}
public:
priority_queue()
{}
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
{
while (first != last)
{
_con.push_back(*first);
++first;
}
// 建堆
for (int i = (_con.size()-1-1)/2; i >= 0; i--)
{
AdjustDown(i);
}
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
void push(const T& x)
{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
const T& top()
{
return _con[0];
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
反向迭代器的实现剖析
rbegin实际指向end位置,rend实际指向begin位置(需注意,rbegin并非最后一个有效元素的位置)。
namespace renshen
{
template<class Iterator, class Ref, class Ptr>
struct ReverseIterator
{
typedef ReverseIterator<Iterator, Ref, Ptr> Self;
Iterator _it;
ReverseIterator(Iterator it)
:_it(it)
{}
Ref operator*()
{
Iterator tmp = _it;
return *(--tmp);
} // 反向迭代器解引用为前一个位置
Ptr operator->()
{
return &(operator*());
} // 编译器会自动利用返回的指针进行->操作
Self& operator++()
{
--_it;
return *this;
}
Self& operator--()
{
++_it;
return *this;
}
bool operator!=(const Self& s) const
{
return _it != s._it;
}
};
}
引申:重载运算符的结合性和优先级与原运算符一致。
仿函数(函数对象)
此类对象可像函数一样使用。示例:
template<class T>
class Less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
// 调用方式:
Less lesstest;
lesstest(1,2) 或 lesstest.operator()(1,2)
引申:标准库中的less
用于升序,greater 用于降序。
作业板块
力扣 155. 最小栈
思路:使用两个栈,一个用于存储数据,另一个用于存储最小值。当插入元素小于等于当前最小值栈顶时,将该元素压入最小值栈;弹出元素与最小值栈顶相同时,弹出最小值栈顶元素。
class MinStack {
public:
void push(int val) {
_st.push(val);
if(_min.empty()||val<=_min.top()) _min.push(val);
}
void pop() {
if(_st.top() == _min.top()) _min.pop();
_st.pop();
}
int top() {
return _st.top();
}
int getMin() {
return _min.top();
}
private:
stack<int>_st;
stack<int>_min;
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(val);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/
牛客网 栈的弹出压入序列
思路:若栈顶元素与出栈序列元素不匹配,则压入数据;匹配则弹出数据。若全部压入后栈中仍有数据,说明序列不合法。
class Solution {
public:
bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
stack<int> st;
int pushi = 0;
int popi = 0;
while (pushi <= pushV.size() - 1) {
st.push(pushV[pushi++]);
while (!st.empty() && st.top() == popV[popi]) {
popi++;
st.pop();
}
}
return popi == pushi;
}
};
力扣 102. 二叉树的层序遍历 & 107. 二叉树的层序遍历 II
-
- 二叉树的层序遍历
思路:使用队列,注意处理根节点为空的情况。
- 二叉树的层序遍历
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>>vv;
queue<TreeNode*>q; int levelsize = 0;
if(root)
{
q.push(root); levelsize = 1;
}
while(!q.empty())
{
vector<int> v;
for(int i = 1;i<=levelsize;i++)
{
TreeNode* j = q.front(); q.pop();
v.push_back(j->val);
if(j->left) q.push(j->left);
if(j->right) q.push(j->right);
}
levelsize = q.size();
vv.push_back(v);
}
return vv;
}
};
-
- 二叉树的层序遍历 II
思路:在上一题基础上,对结果进行反转。
- 二叉树的层序遍历 II
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root) {
vector<vector<int>>vv;
queue<TreeNode*>q; int levelsize = 0;
if(root)
{
q.push(root); levelsize = 1;
}
while(!q.empty())
{
vector<int> v;
for(int i = 1;i<=levelsize;i++)
{
TreeNode* j = q.front(); q.pop();
v.push_back(j->val);
if(j->left) q.push(j->left);
if(j->right) q.push(j->right);
}
levelsize = q.size();
vv.push_back(v);
}
reverse(vv.begin(),vv.end());
return vv;
}
};



