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

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

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

cpp 复制代码
#pragma once
#include <deque>  // 底层容器默认用deque

namespace syj {
template <class T, class Container = deque<T>>  // 模板参数:数据类型+底层容器
class stack {
public:
    // 1. 尾部插入(压栈):直接调用底层容器的push_back
    void 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
cpp 复制代码
#include "Stack.h"
using namespace syj;

class MinStack {
public:
    void push(int x) {
        _elem.push(x);
        // 若_min为空,或x≤当前最小值,压入_min
        if (_min.empty() || x <= _min.top()) {
            _min.push(x);
        }
    }

    void pop() {
        if (_elem.empty()) return;
        // 若弹出的是当前最小值,同步弹出_min
        if (_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,无需括号优先级。
思路:用栈存储数字,遇到运算符时弹出两个数字计算,结果再压栈。

cpp 复制代码
#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()

cpp 复制代码
#pragma once
#include <deque>  // 底层容器默认用deque

namespace syj {
template <class T, class Container = deque<T>>
class queue {
public:
    // 1. 尾部插入(入队):调用底层容器的push_back
    void 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
cpp 复制代码
#include "Queue.h"
using namespace syj;

class MyStack {
public:
    void push(int x) {
        q1.push(x);  // 压栈:直接入q1
    }

    int pop() {
        // 把q1前n-1个元素移到q2
        while (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):

cpp 复制代码
#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)/2
        while (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+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;
            }
        }
    }

    // 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) ,通过切换比较器实现大堆/小堆:

cpp 复制代码
#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个最大元素:

cpp 复制代码
#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),覆盖栈、队列、优先级队列的所有核心接口:

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;  // 3
    cout << "栈顶:" << s.top() << endl;    // 30

    s.pop();
    cout << "pop后栈顶:" << s.top() << endl;  // 20

    cout << "栈是否为空:" << (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;  // 3
    cout << "队头:" << q.front() << endl;    // 10
    cout << "队尾:" << q.back() << endl;     // 30

    q.pop();
    cout << "pop后队头:" << q.front() << endl;  // 20

    cout << "队列是否为空:" << (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;  // 5
    maxHeap.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;  // 1
    minHeap.pop();
    cout << "pop后小堆顶:" << minHeap.top() << endl;  // 3

    cout << 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不返回元素、空容器判断等高频错误。

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

相关推荐
七夜zippoe7 小时前
仓颉FFI实战:C/C++互操作与性能优化
c语言·c++·性能优化
西哥写代码8 小时前
基于dcmtk的dicom工具 第十三章 dicom文件导出bmp、jpg、png、tiff、mp4
c++·mfc·dicom·dcmtk·tiffopen·dipngplugin·dijpegplugin
十五学长8 小时前
程序设计C语言
c语言·开发语言·笔记·学习·考研
永远有缘9 小时前
Java、Python、C# 和 C++ 在函数定义语法上的主要区别
java·c++·python·c#
绛洞花主敏明10 小时前
Go切片的赋值
c++·算法·golang
纵有疾風起14 小时前
C++—string(1):string类的学习与使用
开发语言·c++·经验分享·学习·开源·1024程序员节
Molesidy14 小时前
【随笔】【QT】QT5.15.2版本的最新下载方式!!!
开发语言·qt
二进制person15 小时前
Java EE初阶 --多线程2
java·开发语言
yue00815 小时前
C#理论学习-WinForm实践开发教程总结
开发语言·学习·c#