当前位置: 首页 > news >正文

《从适配器本质到面试题:一文掌握 C++ 栈、队列与优先级队列核心》

《从适配器本质到面试题:一文掌握 C++ 栈、队列与优先级队列核心》

    • 前言
    • 1. 容器适配器:理解栈和队列的“本质”
      • 1.1 适配器的核心逻辑
      • 1.2 STL的默认选择:为什么用deque做底层?
    • 2. 栈(stack):后进先出的“数据抽屉”
      • 2.1 核心接口
      • 2.2 经典实战场景
        • 场景1:最小栈(O(1)获取最小值)
        • 场景2:逆波兰表达式求值
    • 3. 队列(queue):先进先出的“排队模型”
      • 3.1 核心接口
      • 3.2 经典实战场景:用队列实现栈
    • 4. 优先级队列(priority_queue):按“权重”排序的队列
      • 4.1 核心接口
      • 4.2 关键知识点:大堆与小堆切换
      • 4.3 经典实战场景:数组中第K个最大元素
    • 5. 源码测试:一站式验证所有接口
      • 测试结果预期
    • 6. 常见面试题与避坑指南
      • 6.1 高频面试题
      • 6.2 避坑指南
    • 总结

前言

在C++开发中,栈(stack)、队列(queue)和优先级队列(priority_queue)是高频使用的数据结构,但很多开发者只停留在“会用”层面,遇到性能瓶颈或自定义需求时就束手无策。本文从实际开发痛点出发,带你吃透这三种数据结构的底层逻辑、接口设计、实战场景及避坑指南。

1. 容器适配器:理解栈和队列的“本质”

很多人误以为栈和队列是“独立容器”,但实际上它们是容器适配器(Container Adapter) ——就像“手机充电器”:本身不产生电力,而是将插座的接口转换成手机需要的接口。

1.1 适配器的核心逻辑

容器适配器不直接存储数据,而是封装底层容器(如vector、deque、list),并对外提供简化的接口。比如:

  • 栈只需要“尾部插入/删除”,所以封装底层容器的push_back()pop_back()
  • 队列需要“尾部插入、头部删除”,所以封装push_back()pop_front()

1.2 STL的默认选择:为什么用deque做底层?

STL中栈和队列默认用deque(双端队列)作为底层容器,而非vector或list,原因很简单:

场景vector缺陷list缺陷deque优势
栈扩容需搬移大量数据无连续空间,缓存命中率低分段连续,扩容无需搬移
队列头删需搬移所有元素(O(n))空间利用率低(存指针)头删O(1),空间利用率高

一句话总结:deque兼顾了vector的“连续空间效率”和list的“两端操作灵活性”,完美适配栈和队列的需求。

2. 栈(stack):后进先出的“数据抽屉”

栈的核心特性是LIFO(Last In First Out,后进先出) ——就像叠盘子:最后放的盘子,最先被拿走。

2.1 核心接口

以下是基于deque封装的栈源码(Stack.h):

#pragma once
#include <deque>  // 底层容器默认用dequenamespace syj {
template <class T, class Container = deque<T>>  // 模板参数:数据类型+底层容器
class stack {
public:// 1. 尾部插入(压栈):直接调用底层容器的push_backvoid push(const T& x) { _con.push_back(x); }// 2. 尾部删除(出栈):注意!pop不返回元素(避免拷贝开销+异常安全)void pop() { // 易错点:pop前必须判断栈是否为空,否则会崩溃if (empty()) throw "stack is empty!"; _con.pop_back(); }// 3. 获取栈顶元素:返回尾部元素的引用const T& top() const { if (empty()) throw "stack is empty!"; return _con.back(); }// 4. 辅助接口:判空、获取大小size_t size() const { return _con.size(); }bool empty() const { return _con.empty(); }private:Container _con;  // 封装底层容器,对外隐藏实现
};
}  // namespace syj

2.2 经典实战场景

场景1:最小栈(O(1)获取最小值)

痛点:普通栈获取最小值需要遍历(O(n)),如何优化到O(1)?
思路:用两个栈——一个存数据(_elem),一个存当前最小值(_min):

  • 压栈时:若新元素≤_min栈顶,同步压入_min
  • 出栈时:若_elem栈顶等于_min栈顶,同步弹出_min
#include "Stack.h"
using namespace syj;class MinStack {
public:void push(int x) {_elem.push(x);// 若_min为空,或x≤当前最小值,压入_minif (_min.empty() || x <= _min.top()) {_min.push(x);}}void pop() {if (_elem.empty()) return;// 若弹出的是当前最小值,同步弹出_minif (_elem.top() == _min.top()) {_min.pop();}_elem.pop();}int top() { return _elem.top(); }int getMin() { return _min.top(); }  // O(1)获取最小值private:stack<int> _elem;  // 数据栈stack<int> _min;   // 最小值栈
};
场景2:逆波兰表达式求值

逆波兰表达式(后缀表达式):如“3 4 +”表示3+4,无需括号优先级。
思路:用栈存储数字,遇到运算符时弹出两个数字计算,结果再压栈。

#include "Stack.h"
#include <vector>
#include <string>
using namespace syj;class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> s;for (auto& str : tokens) {// 若为运算符,弹出两个数字计算if (str == "+" || str == "-" || str == "*" || str == "/") {int right = s.top(); s.pop();  // 注意顺序:右操作数先弹出int left = s.top(); s.pop();switch (str[0]) {case '+': s.push(left + right); break;case '-': s.push(left - right); break;case '*': s.push(left * right); break;case '/': s.push(left / right); break;  // 题目保证无除数为0}} else {// 若为数字,转成int压栈s.push(stoi(str));}}return s.top();  // 最终结果在栈顶}
};

3. 队列(queue):先进先出的“排队模型”

队列的核心特性是FIFO(First In First Out,先进先出) ——就像银行排队:先到的人先办理业务。

3.1 核心接口

以下是基于deque封装的队列源码(Queue.h),注意头删用pop_front()

#pragma once
#include <deque>  // 底层容器默认用dequenamespace syj {
template <class T, class Container = deque<T>>
class queue {
public:// 1. 尾部插入(入队):调用底层容器的push_backvoid push(const T& x) { _con.push_back(x); }// 2. 头部删除(出队):注意!pop不返回元素void pop() { if (empty()) throw "queue is empty!"; _con.pop_front();  // 队列核心:头部删除}// 3. 获取队头/队尾元素const T& front() const {  // 队头:最先入队的元素if (empty()) throw "queue is empty!"; return _con.front(); }const T& back() const {   // 队尾:最后入队的元素if (empty()) throw "queue is empty!"; return _con.back(); }// 4. 辅助接口size_t size() const { return _con.size(); }bool empty() const { return _con.empty(); }private:Container _con;  // 封装底层容器
};
}  // namespace syj

3.2 经典实战场景:用队列实现栈

面试高频题:仅用队列的接口(push、pop、front等)实现栈的功能。
思路:用两个队列——q1存数据,q2做临时中转:

  • 压栈:直接入q1
  • 出栈:将q1前n-1个元素移到q2,弹出q1最后一个元素,再交换q1q2
#include "Queue.h"
using namespace syj;class MyStack {
public:void push(int x) {q1.push(x);  // 压栈:直接入q1}int pop() {// 把q1前n-1个元素移到q2while (q1.size() > 1) {q2.push(q1.front());q1.pop();}// 弹出q1最后一个元素(栈顶)int topVal = q1.front();q1.pop();// 交换q1和q2,让q1始终存数据swap(q1, q2);return topVal;}int top() {return q1.back();  // 队尾就是栈顶}bool empty() {return q1.empty();}private:queue<int> q1;  // 主队列:存数据queue<int> q2;  // 辅助队列:临时中转
};

4. 优先级队列(priority_queue):按“权重”排序的队列

优先级队列本质是堆(Heap) ——每次出队的不是“最早入队的元素”,而是“优先级最高的元素”(默认大堆:最大值优先)。

4.1 核心接口

优先级队列底层用vector存储数据,并通过堆算法(向上调整、向下调整)维护堆结构(Priority_queue.h):

#pragma once
#include <vector>
#include <algorithm>  // 用于swap// 比较规则:默认大堆(Less:父节点 < 子节点时交换)
template<class T>
class Less {
public:bool operator()(const T& x, const T& y) {return x < y;  // 大堆:x(父)< y(子)→ 交换}
};// 小堆比较规则(Grate:父节点 > 子节点时交换)
template<class T>
class Grate {
public:bool operator()(const T& x, const T& y) {return x > y;  // 小堆:x(父)> y(子)→ 交换}
};namespace syj {
template <class T, class Container = vector<T>, class Comper = Less<T>>
class Priority_queue {
public:// 1. 向上调整:插入元素后维护堆(从子节点到父节点)void AdjustUp(int child) {Comper com;  // 比较器:灵活切换大堆/小堆int parent = (child - 1) / 2;  // 父节点下标 = (子节点-1)/2while (child > 0) {// 若父节点优先级低于子节点,交换if (com(_con[parent], _con[child])) {swap(_con[child], _con[parent]);child = parent;          // 向上移动子节点指针parent = (child - 1) / 2;// 更新父节点指针} else {break;  // 堆结构正常,退出}}}// 2. 向下调整:删除元素后维护堆(从父节点到子节点)void AdjustDown(int parent) {Comper com;size_t child = parent * 2 + 1;  // 左子节点下标 = 父节点*2+1while (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;}}}// 3. 插入元素:尾插后向上调整void push(const T& x) {_con.push_back(x);AdjustUp(_con.size() - 1);  // 从最后一个元素(新子节点)开始调整}// 4. 删除元素:堆顶与尾元素交换,尾删后向下调整void pop() {if (empty()) throw "priority_queue is empty!";swap(_con[0], _con[_con.size() - 1]);  // 堆顶(优先级最高)与尾元素交换_con.pop_back();                       // 删除尾元素(原堆顶)AdjustDown(0);                         // 从堆顶开始向下调整}// 5. 获取堆顶元素(优先级最高)const T& top() const {if (empty()) throw "priority_queue is empty!";return _con[0];  // 堆顶就是vector[0]}// 6. 辅助接口size_t size() const { return _con.size(); }bool empty() const { return _con.empty(); }private:Container _con;  // 底层容器:vector(支持随机访问,适合堆调整)
};
}  // namespace syj

4.2 关键知识点:大堆与小堆切换

优先级队列的核心是比较器(Comper) ,通过切换比较器实现大堆/小堆:

#include "Priority_queue.h"
using namespace syj;void testPriorityQueue() {// 1. 大堆(默认Less,最大值优先)Priority_queue<int> maxHeap;maxHeap.push(3); maxHeap.push(1); maxHeap.push(5);cout << "大堆顶:" << maxHeap.top() << endl;  // 输出5// 2. 小堆(指定Grate,最小值优先)Priority_queue<int, vector<int>, Grate<int>> minHeap;minHeap.push(3); minHeap.push(1); minHeap.push(5);cout << "小堆顶:" << minHeap.top() << endl;  // 输出1
}

4.3 经典实战场景:数组中第K个最大元素

题目:在未排序的数组中找到第K个最大的元素(如[3,2,1,5,6,4],K=2 → 5)。
思路:用小堆存储前K个最大元素,堆顶就是第K个最大元素:

#include "Priority_queue.h"
using namespace syj;class Solution {
public:int findKthLargest(vector<int>& nums, int k) {// 小堆:存储前K个最大元素Priority_queue<int, vector<int>, Grate<int>> minHeap;for (int num : nums) {minHeap.push(num);// 若堆大小超过K,弹出最小值(保证堆内是前K大)if (minHeap.size() > k) {minHeap.pop();}}return minHeap.top();  // 堆顶就是第K个最大元素}
};

5. 源码测试:一站式验证所有接口

为了方便验证,我们编写一个测试文件(TestAll.cpp),覆盖栈、队列、优先级队列的所有核心接口:

#include <iostream>
#include <vector>
#include "Stack.h"
#include "Queue.h"
#include "Priority_queue.h"
using namespace std;
using namespace syj;// 测试栈
void testStack() {cout << "=== 测试栈 ===" << endl;stack<int> s;s.push(10);s.push(20);s.push(30);cout << "栈大小:" << s.size() << endl;  // 3cout << "栈顶:" << s.top() << endl;    // 30s.pop();cout << "pop后栈顶:" << s.top() << endl;  // 20cout << "栈是否为空:" << (s.empty() ? "是" : "否") << endl;  // 否cout << endl;
}// 测试队列
void testQueue() {cout << "=== 测试队列 ===" << endl;queue<int> q;q.push(10);q.push(20);q.push(30);cout << "队列大小:" << q.size() << endl;  // 3cout << "队头:" << q.front() << endl;    // 10cout << "队尾:" << q.back() << endl;     // 30q.pop();cout << "pop后队头:" << q.front() << endl;  // 20cout << "队列是否为空:" << (q.empty() ? "是" : "否") << endl;  // 否cout << endl;
}// 测试优先级队列
void testPriorityQueue() {cout << "=== 测试优先级队列 ===" << endl;// 大堆Priority_queue<int> maxHeap;maxHeap.push(3); maxHeap.push(1); maxHeap.push(5);cout << "大堆顶:" << maxHeap.top() << endl;  // 5maxHeap.pop();cout << "pop后大堆顶:" << maxHeap.top() << endl;  // 3// 小堆Priority_queue<int, vector<int>, Grate<int>> minHeap;minHeap.push(3); minHeap.push(1); minHeap.push(5);cout << "小堆顶:" << minHeap.top() << endl;  // 1minHeap.pop();cout << "pop后小堆顶:" << minHeap.top() << endl;  // 3cout << endl;
}int main() {testStack();testQueue();testPriorityQueue();return 0;
}

测试结果预期

=== 测试栈 ===
栈大小:3
栈顶:30
pop后栈顶:20
栈是否为空:否=== 测试队列 ===
队列大小:3
队头:10
队尾:30
pop后队头:20
队列是否为空:否=== 测试优先级队列 ===
大堆顶:5
pop后大堆顶:3
小堆顶:1
pop后小堆顶:3

6. 常见面试题与避坑指南

6.1 高频面试题

  1. 用两个栈实现队列:类似“用两个队列实现栈”,一个栈存数据,一个栈中转(入队时压栈1,出队时将栈1元素移到栈2,弹出栈2顶)。
  2. 栈的弹出压入序列验证:用栈模拟入栈过程,每次入栈后检查是否与弹出序列匹配,匹配则弹出。
  3. 优先级队列自定义类型排序:需重载比较器,如按日期排序。

6.2 避坑指南

  1. pop()不返回元素:栈、队列、优先级队列的pop()都不返回元素,若要获取元素,需先调用top()/front()
    • 错误:int val = s.pop();(编译失败)
    • 正确:int val = s.top(); s.pop();
  2. 空容器操作崩溃top()/front()/pop()前必须用empty()判断,否则会访问非法内存。
  3. 优先级队列的比较器逻辑:大堆用Less(父<子交换),小堆用Grate(父>子交换),不要记反!

总结

本文从“解决实际问题”出发,带你掌握了栈、队列、优先级队列的:

  • 本质:容器适配器,封装底层容器实现简化接口;
  • 源码:完整可运行的封装代码,关键逻辑标注注释;
  • 实战:最小栈、逆波兰表达式、第K大元素等经典场景;
  • 避坑:pop不返回元素、空容器判断等高频错误。

如果有疑问或需要扩展场景,欢迎在评论区交流!

http://www.dtcms.com/a/565331.html

相关文章:

  • 心理咨询网站模板做网站手机
  • 光学3D表面轮廓仪中Rz代表什么?如何精准测量Rz?
  • ps做登录网站北京网站制作工作室
  • git rebase提交
  • vue3引入icon-font
  • 基于开源操作系统搭建K8S高可用集群
  • 学做网站论坛 可以吗做网站是不是太麻烦了
  • leetcode 1578 使绳子变成彩色的最短时间
  • 中国建设银行网上银行官方网站长沙优秀网站建设
  • 1.7 Foundry介绍
  • 什么是向量数据库?主流产品介绍与实战演练
  • redission实现延时队列
  • 浏览器端缓存地图请求:使用 IndexedDB + ajax-hook 提升地图加载速度
  • 地铁工程建设论文投稿网站谷歌广告代运营
  • 广东备案网站软件开发怎么学
  • 【成长纪实】鸿蒙 ArkTS 语言从零到一完整指南
  • PyTorch模型部署实战:从TorchScript到LibTorch的完整路径
  • 网站开发后台结构江西建设职业技术学院网站
  • 如何导出VSCode的已安装扩展列表?
  • 高级系统架构师笔记——系统质量属性与架构评估(1)软件系统质量属性
  • Vscode参数设置及使用记录ubuntu2204(更新中)
  • Linux上vscode c/c++开发环境搭建详细-abuild
  • vscode多文件编程bug记录
  • 分布式答案解析
  • 做耳机套的网站常用网站推广方法的适用性
  • 网站建设增长率呼和浩特建设厅网站
  • AI 音乐工具 Suno 和 Producer 对比
  • KeilIDE背后的命令
  • flash中文网站模板带有flash的网站
  • 阿里云核心服务解析与应用实践