de风——【从零开始学C++】(十二):stack和queue的基本使用和模拟实现


专栏说明:本专栏专为 C++ 新手小白打造,从零基础开始,循序渐进讲解 C++ 核心知识点。每篇文章力求用大白话讲解,配合完整可运行的代码示例,让你轻松入门 C++!

C++专栏链接🔗

https://blog.csdn.net/xiao_running/category_13158015.html?fromshare=blogcolumn&sharetype=blogcolumn&sharerId=13158015&sharerefer=PC&sharesource=Xiao_running&sharefrom=from_link

📌 面试重点标注:本文中 🔴 标记为面试高频考点,请重点掌握!


前言

呦呦呦!我~是~你~们~的~小小风呀,你们好呀!今天我们来学习 STL 中两个非常重要的容器适配器 ------stack(栈)和queue(队列)

为什么叫 "容器适配器" 呢?这是因为 stack 和 queue 本身并不是真正的容器,它们是在其他容器(比如 deque)的基础上,封装了一层特定的接口。这就涉及到我们今天要讲的第一个重要概念 ------适配器模式

在正式学习 stack 和 queue 之前,我们先搞懂两个前置知识:适配器模式和 deque 容器。


第一部分:前置知识 ------ 适配器模式和 deque 简介

(一)适配器模式的概念

1. 简介作用

适配器模式 ,说白了就是 "转换器" 的意思。

举个生活中的例子:

  • 你买了一个港版的 iPhone,充电器是三脚英标插头

  • 但家里的插座都是国标两孔的

  • 怎么办?买一个转换头!一头插英标插头,一头插国标插座

  • 这个转换头,就是 "适配器"

在编程中,适配器模式也是一样的道理:

  • 我们已经有了一个现成的类(比如 deque),它有很多功能

  • 但我们现在只需要它的一部分功能,而且想用特定的方式来调用

  • 于是我们写一个 "包装类",把原来的类包起来,只对外暴露我们需要的接口

  • 这个包装类,就是适配器

🔴 面试考点 :适配器模式的核心思想是组合复用,而不是继承。通过封装已有容器来实现新的功能,而不是重新写一遍。

2. 代码例子:生活中的适配器模式
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 已有的类:三孔插头
class ThreePinPlug {
public:
    void useThreePin() {
        cout << "使用三孔插头充电" << endl;
    }
};

// 适配器:转换头
class PlugAdapter {
private:
    ThreePinPlug threePin;  // 组合,而不是继承!
public:
    // 对外只暴露两孔的接口
    void useTwoPin() {
        cout << "通过转换头,";
        threePin.useThreePin();  // 内部还是调用三孔的功能
    }
};

int main() {
    PlugAdapter adapter;
    adapter.useTwoPin();  // 用户只需要知道两孔怎么用就行
    return 0;
}

运行结果:

bash 复制代码
通过转换头,使用三孔插头充电

代码说明:

  • ThreePinPlug 是已有的功能强大的类

  • PlugAdapter 是适配器,它包含了一个 ThreePinPlug 对象(组合)

  • 对外只提供useTwoPin()接口,隐藏了内部的实现

  • 这就是 stack 和 queue 的实现原理!

3. 新手坑点提示

常见误区:很多新手以为适配器是 "继承" 来的,其实不是!

  • 适配器用的是组合(contains) ,不是继承(is a)

  • stack 不是 deque 的子类,stack 里面 "装着" 一个 deque


(二)deque 容器的简单介绍

1. 简介作用

deque (发音 "deck"),全称是double-ended queue,双端队列。

简单来说:

  • deque = vector 的优点 + list 的优点

  • 既可以像 vector 一样随机访问,又可以像 list 一样头尾插入删除都很快

为什么 deque 是 stack 和 queue 的默认底层容器? 🔴【面试高频】

因为 stack 只需要在一端 操作(尾插尾删),queue 只需要在两端操作(尾插头删):

  1. vector:尾插尾删 O (1),但头插头删 O (n) → 适合做 stack,不适合做 queue

  2. list:头尾操作都是 O (1),但不支持随机访问,空间不连续 → 性能不如 deque

  3. deque :头尾操作都是 O (1),支持随机访问,空间相对连续 → 完美!

2. deque 的优缺点

|--------------------------|----------------|
| 优点 | 缺点 |
| 头尾插入删除都是 O (1) | 中间插入删除效率一般 |
| 支持随机访问 \[\] | 迭代器比 vector 复杂 |
| 扩容比 vector 高效(不需要拷贝所有元素) | 内存占用略高 |
| 空间利用率比 list 高 | 遍历速度略慢于 vector |

3. 代码例子:deque 的基本使用
cpp 复制代码
#include <iostream>
#include <deque>
using namespace std;

int main() {
    deque<int> dq;
    
    // 1. 尾插尾删(stack用的就是这个!)
    dq.push_back(1);
    dq.push_back(2);
    dq.push_back(3);
    cout << "尾插后:";
    for (int x : dq) cout << x << " ";  // 1 2 3
    cout << endl;
    
    dq.pop_back();
    cout << "尾删后:";
    for (int x : dq) cout << x << " ";  // 1 2
    cout << endl;
    
    // 2. 头插头删(queue用的就是这个!)
    dq.push_front(0);
    cout << "头插后:";
    for (int x : dq) cout << x << " ";  // 0 1 2
    cout << endl;
    
    dq.pop_front();
    cout << "头删后:";
    for (int x : dq) cout << x << " ";  // 1 2
    cout << endl;
    
    // 3. 随机访问
    cout << "第一个元素:" << dq[0] << endl;  // 1
    cout << "第二个元素:" << dq[1] << endl;  // 2
    
    return 0;
}

运行结果:

cpp 复制代码
尾插后:1 2 3 
尾删后:1 2 
头插后:0 1 2 
头删后:1 2 
第一个元素:1
第二个元素:2

代码说明:

  • push_back/pop_back:尾部操作,stack 用

  • push_front/pop_front:头部操作,queue 用

  • operator[]:随机访问,这是 list 做不到的

💡 新手提示 :你现在明白为什么 deque 是默认底层了吧!stack 需要的push_back/pop_back,queue 需要的push_back/pop_front,deque 全都有,而且效率都很高!


第二部分:stack 的基本使用和模拟实现

(一)stack 的基本使用

1. stack 的特点(后进先出 LIFO)

简介作用: stack(栈)是一种后进先出(Last In First Out, LIFO)的数据结构。

想象一下:

  • 往枪的弹夹里压子弹,先压进去的子弹最后打出来

  • 最后压进去的子弹,第一个打出来

  • 这就是栈!

特点:

  • ✅ 只允许在栈顶(尾部)插入和删除

  • 不允许遍历!(没有迭代器)

  • ❌ 不支持随机访问

  • 只能看栈顶元素

🔴 面试考点:stack 是后进先出,只能在一端操作。

2. stack 的构造函数

代码例子:stack 的构造

cpp 复制代码
#include <iostream>
#include <stack>
#include <vector>
#include <list>
using namespace std;

int main() {
    // 1. 默认构造(底层用deque)
    stack<int> s1;
    
    // 2. 指定底层容器为vector
    stack<int, vector<int>> s2;
    
    // 3. 指定底层容器为list
    stack<int, list<int>> s3;
    
    // 4. 用已有容器初始化
    vector<int> v = {1, 2, 3};
    stack<int, vector<int>> s4(v);  // 把v的元素全部入栈
    
    cout << "s4的大小:" << s4.size() << endl;  // 3
    cout << "s4的栈顶:" << s4.top() << endl;   // 3(最后一个元素在栈顶)
    
    return 0;
}

运行结果:

cpp 复制代码
s4的大小:3
s4的栈顶:3
3. stack 的常用接口
【示例 1】push、pop、top 操作
cpp 复制代码
#include <iostream>
#include <stack>
using namespace std;

int main() {
    stack<int> s;
    
    // push:入栈(从栈顶压入)
    s.push(1);
    s.push(2);
    s.push(3);
    
    cout << "栈的大小:" << s.size() << endl;  // 3
    cout << "栈是否为空:" << (s.empty() ? "是" : "否") << endl;  // 否
    
    // top:获取栈顶元素(不删除)
    cout << "栈顶元素:" << s.top() << endl;  // 3
    
    // pop:出栈(删除栈顶元素,注意:不返回值!)
    s.pop();
    cout << "pop一次后,栈顶:" << s.top() << endl;  // 2
    
    s.pop();
    s.pop();
    cout << "全部pop后,栈是否为空:" << (s.empty() ? "是" : "否") << endl;  // 是
    
    return 0;
}

运行结果:

cpp 复制代码
栈的大小:3
栈是否为空:否
栈顶元素:3
pop一次后,栈顶:2
全部pop后,栈是否为空:是
【示例 2】stack 的经典应用:括号匹配
cpp 复制代码
#include <iostream>
#include <stack>
#include <string>
using namespace std;

bool isValid(string s) {
    stack<char> st;
    for (char c : s) {
        if (c == '(' || c == '[' || c == '{') {
            st.push(c);  // 左括号入栈
        } else {
            if (st.empty()) return false;  // 没有匹配的左括号
            char top = st.top();
            st.pop();
            // 检查是否匹配
            if ((c == ')' && top != '(') ||
                (c == ']' && top != '[') ||
                (c == '}' && top != '{')) {
                return false;
            }
        }
    }
    return st.empty();  // 所有左括号都匹配了
}

int main() {
    cout << isValid("()[]{}") << endl;  // 1(true)
    cout << isValid("([)]") << endl;    // 0(false)
    cout << isValid("{[]}") << endl;    // 1(true)
    return 0;
}
4. 经典 Bug 和坑点 ⚠️

坑点 1:对空栈调用 top () 或 pop ()

cpp 复制代码
stack<int> s;
s.top();  // 崩溃!未定义行为
s.pop();  // 崩溃!未定义行为

解决: 调用前一定要检查empty()

坑点 2:试图遍历 stack

cpp 复制代码
stack<int> s;
// 错误!stack没有begin()和end(),不支持范围for
for (int x : s) { ... }

原因: 栈的设计就是只能访问栈顶,不允许遍历。要遍历的话用 vector!

坑点 3:以为 pop () 会返回值

cpp 复制代码
int val = s.pop();  // 错误!pop()返回值是void

正确写法:

cpp 复制代码
int val = s.top();
s.pop();

(二)stack 的模拟实现

1. 模板参数设计

简介作用: 我们自己实现一个 stack,核心就是:

  1. 模板参数第一个是数据类型 T

  2. 第二个模板参数是底层容器 Container,默认用 deque

  3. 所有接口都调用底层容器的接口

这就是适配器模式的精髓!我们不用自己写数据结构,只要 "转发" 调用就行。

2. 常用接口的封装实现

完整代码:my_stack 的实现

cpp 复制代码
#include <iostream>
#include <deque>
using namespace std;

// 模拟实现stack
template<class T, class Container = deque<T>>  // 默认底层用deque
class my_stack {
private:
    Container _con;  // 底层容器对象(组合!不是继承!)
    
public:
    // 1. 入栈:尾插
    void push(const T& val) {
        _con.push_back(val);  // 直接调用底层容器的接口
    }
    
    // 2. 出栈:尾删
    void pop() {
        _con.pop_back();  // 直接调用底层容器的接口
    }
    
    // 3. 获取栈顶:最后一个元素
    T& top() {
        return _con.back();  // 直接调用底层容器的接口
    }
    
    const T& top() const {
        return _con.back();
    }
    
    // 4. 栈的大小
    size_t size() const {
        return _con.size();
    }
    
    // 5. 栈是否为空
    bool empty() const {
        return _con.empty();
    }
};

// 测试代码
int main() {
    my_stack<int> s;
    
    // 测试push
    s.push(1);
    s.push(2);
    s.push(3);
    
    cout << "栈大小:" << s.size() << endl;    // 3
    cout << "栈顶元素:" << s.top() << endl;   // 3
    
    // 测试pop
    s.pop();
    cout << "pop后栈顶:" << s.top() << endl;  // 2
    
    // 测试遍历出栈
    cout << "出栈顺序:";
    while (!s.empty()) {
        cout << s.top() << " ";
        s.pop();
    }
    cout << endl;  // 2 1
    
    cout << "栈是否为空:" << (s.empty() ? "是" : "否") << endl;  // 是
    
    return 0;
}

运行结果:

cpp 复制代码
栈大小:3
栈顶元素:3
pop后栈顶:2
出栈顺序:2 1 
栈是否为空:是

代码说明:

  • 看到了吗?我们的 my_stack一行数据结构代码都没写

  • 所有功能都是调用底层容器_con的接口

  • 这就是适配器模式的威力!复用已有代码,快速实现新功能

💡 新手提示 :你可以把Container = deque<T>改成vector<T>或者list<T>,代码完全不用改!这就是模板的妙处!


第三部分:queue 的基本使用和模拟实现

(一)queue 的基本使用

1. queue 的特点(先进先出 FIFO)

简介作用: queue(队列)是一种先进先出(First In First Out, FIFO)的数据结构。

想象一下:

  • 排队买奶茶,先排队的人先买到

  • 后来的人排在队尾

  • 这就是队列!

特点:

  • ✅ 只允许在队尾 (尾部)插入,在队头(头部)删除

  • 不允许遍历!(没有迭代器)

  • ❌ 不支持随机访问

  • 只能看队头和队尾元素

🔴 面试考点:queue 是先进先出,在队尾入队,队头出队。

2. queue 的构造函数

代码例子:queue 的构造

cpp 复制代码
#include <iostream>
#include <queue>
#include <list>
using namespace std;

int main() {
    // 1. 默认构造(底层用deque)
    queue<int> q1;
    
    // 2. 指定底层容器为list(注意:queue不能用vector!因为vector没有pop_front)
    queue<int, list<int>> q2;
    
    // 3. 用已有容器初始化
    list<int> l = {1, 2, 3};
    queue<int, list<int>> q3(l);
    
    cout << "q3的大小:" << q3.size() << endl;    // 3
    cout << "q3的队头:" << q3.front() << endl;   // 1(第一个元素在队头)
    cout << "q3的队尾:" << q3.back() << endl;    // 3(最后一个元素在队尾)
    
    return 0;
}

运行结果:

cpp 复制代码
q3的大小:3
q3的队头:1
q3的队尾:3

⚠️ 重要提示 :queue 不能用 vector 做底层!因为 vector 没有pop_front()(头删)操作!

3. queue 的常用接口
【示例 1】push、pop、front、back 操作
cpp 复制代码
#include <iostream>
#include <queue>
using namespace std;

int main() {
    queue<int> q;
    
    // push:入队(从队尾加入)
    q.push(1);
    q.push(2);
    q.push(3);
    
    cout << "队列大小:" << q.size() << endl;     // 3
    cout << "队列是否为空:" << (q.empty() ? "是" : "否") << endl;  // 否
    
    // front:获取队头元素
    cout << "队头元素:" << q.front() << endl;    // 1
    // back:获取队尾元素
    cout << "队尾元素:" << q.back() << endl;     // 3
    
    // pop:出队(删除队头元素)
    q.pop();
    cout << "pop一次后,队头:" << q.front() << endl;  // 2
    
    q.pop();
    q.pop();
    cout << "全部pop后,队列是否为空:" << (q.empty() ? "是" : "否") << endl;  // 是
    
    return 0;
}

运行结果:

cpp 复制代码
队列大小:3
队列是否为空:否
队头元素:1
队尾元素:3
pop一次后,队头:2
全部pop后,队列是否为空:是
【示例 2】queue 的经典应用:二叉树层序遍历
cpp 复制代码
#include <iostream>
#include <queue>
using namespace std;

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// 层序遍历(广度优先)
void levelOrder(TreeNode* root) {
    if (root == nullptr) return;
    
    queue<TreeNode*> q;
    q.push(root);
    
    while (!q.empty()) {
        TreeNode* node = q.front();
        q.pop();
        
        cout << node->val << " ";  // 访问当前节点
        
        // 左右孩子入队
        if (node->left) q.push(node->left);
        if (node->right) q.push(node->right);
    }
}

int main() {
    // 构建一棵二叉树
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);
    root->left->left = new TreeNode(4);
    root->left->right = new TreeNode(5);
    
    cout << "层序遍历结果:";
    levelOrder(root);  // 1 2 3 4 5
    cout << endl;
    
    return 0;
}
4. 经典 Bug 和坑点 ⚠️

坑点 1:对空队列调用 front ()、back () 或 pop ()

cpp 复制代码
queue<int> q;
q.front();  // 崩溃!
q.back();   // 崩溃!
q.pop();    // 崩溃!

解决: 调用前一定要检查empty()

坑点 2:用 vector 做 queue 的底层容器

cpp 复制代码
queue<int, vector<int>> q;  // 编译错误!
q.push(1);
q.pop();  // vector没有pop_front()!

解决: queue 只能用 deque 或 list 做底层

坑点 3:混淆 stack 和 queue 的出栈 / 出队位置

  • stack:在尾部删除(pop_back)

  • queue:在头部删除(pop_front)


(二)queue 的模拟实现

1. 模板参数设计

简介作用: queue 的实现和 stack 几乎一样,也是适配器模式:

  1. 第一个模板参数是数据类型 T

  2. 第二个模板参数是底层容器 Container,默认用 deque

  3. 区别只是接口不同:queue 需要头删、取队头队尾

2. 常用接口的封装实现

完整代码:my_queue 的实现

cpp 复制代码
#include <iostream>
#include <deque>
using namespace std;

// 模拟实现queue
template<class T, class Container = deque<T>>  // 默认底层用deque
class my_queue {
private:
    Container _con;  // 底层容器对象(还是组合!)
    
public:
    // 1. 入队:尾插
    void push(const T& val) {
        _con.push_back(val);
    }
    
    // 2. 出队:头删(和stack唯一的区别!)
    void pop() {
        _con.pop_front();  // 调用底层的头删
    }
    
    // 3. 获取队头:第一个元素
    T& front() {
        return _con.front();
    }
    
    const T& front() const {
        return _con.front();
    }
    
    // 4. 获取队尾:最后一个元素
    T& back() {
        return _con.back();
    }
    
    const T& back() const {
        return _con.back();
    }
    
    // 5. 队列大小
    size_t size() const {
        return _con.size();
    }
    
    // 6. 队列是否为空
    bool empty() const {
        return _con.empty();
    }
};

// 测试代码
int main() {
    my_queue<int> q;
    
    // 测试push
    q.push(1);
    q.push(2);
    q.push(3);
    
    cout << "队列大小:" << q.size() << endl;      // 3
    cout << "队头元素:" << q.front() << endl;     // 1
    cout << "队尾元素:" << q.back() << endl;      // 3
    
    // 测试pop
    q.pop();
    cout << "pop一次后队头:" << q.front() << endl;  // 2
    
    // 测试遍历出队
    cout << "出队顺序:";
    while (!q.empty()) {
        cout << q.front() << " ";
        q.pop();
    }
    cout << endl;  // 2 3
    
    cout << "队列是否为空:" << (q.empty() ? "是" : "否") << endl;  // 是
    
    return 0;
}

运行结果:

cpp 复制代码
队列大小:3
队头元素:1
队尾元素:3
pop一次后队头:2
出队顺序:2 3 
队列是否为空:是

代码说明:

  • 发现了吗?my_queue 和 my_stack 的代码90% 都是一样的

  • 唯一的区别就是:

    • stack 用pop_back()(尾删)

    • queue 用pop_front()(头删)

  • 这就是适配器模式的强大之处!


第四部分:对比总结

(一)stack 和 queue 的对比 🔴【面试必背】

|---------|-------------------|-----------------------------|
| 特性 | stack(栈) | queue(队列) |
| 进出规则 | 后进先出 LIFO | 先进先出 FIFO |
| 插入位置 | 栈顶(尾部) | 队尾(尾部) |
| 删除位置 | 栈顶(尾部) | 队头(头部) |
| 可访问元素 | 只能访问栈顶 top () | 可以访问队头 front () 和队尾 back () |
| 底层删除操作 | pop_back() | pop_front() |
| 支持的底层容器 | vector、list、deque | list、deque(不能用 vector) |
| 迭代器 | 无 | 无 |
| 典型应用 | 括号匹配、表达式求值、函数调用栈 | 层序遍历、消息队列、任务调度 |

💡 记忆口诀

  • 栈:后进先出,只在一端操作

  • 队列:先进先出,两端操作


(二)不同底层容器的选择

1. 三种容器做底层的优缺点对比

|---------------|-----------------------------------------------|----------------------------------------------|
| 底层容器 | 做 stack 的优缺点 | 做 queue 的优缺点 |
| vector | ✅ 随机访问快,空间连续 ✅ push_back/pop_back 效率高 ❌ 扩容有开销 | ❌ 不支持! 没有 pop_front |
| list | ✅ 无扩容问题 ✅ 头尾操作都是 O (1) ❌ 不支持随机访问,空间碎片多 | ✅ 无扩容问题 ✅ 头尾操作都是 O (1) ❌ 不支持随机访问,空间碎片多 |
| deque(默认) | ✅ 头尾操作都是 O (1) ✅ 支持随机访问 ✅ 扩容开销小 ⭐ 综合性能最好 | ✅ 头尾操作都是 O (1) ✅ 支持随机访问 ✅ 扩容开销小 ⭐ 综合性能最好 |

2. 什么时候换底层容器?

一般情况:用默认的 deque 就行! STL 设计者已经帮我们选好了最优解。

特殊场景:

  • 如果你已经有一个 vector,想把它当栈用 → 用stack<int, vector<int>>

  • 如果你需要频繁大量入栈出栈,不想扩容 → 可以考虑用 list 做底层

  • queue 一般不建议换底层,deque 已经很完美了


本篇总结

今天我们学习了 STL 中的两个容器适配器 stack 和 queue,核心要点:

  1. 适配器模式:用组合的方式,封装已有容器,对外提供特定接口。不是继承,是组合!

  2. deque:双端队列,头尾操作都是 O (1),支持随机访问,是 stack 和 queue 的默认底层容器。

  3. stack:后进先出 LIFO,只能在尾部操作,接口:push、pop、top、size、empty

  4. queue:先进先出 FIFO,尾插头删,接口:push、pop、front、back、size、empty

  5. 模拟实现:两个容器的实现都非常简单,都是调用底层容器的接口,这就是适配器模式的威力!


📝 课后作业

  1. 自己动手实现一遍 my_stack 和 my_queue

  2. 用 stack 实现一个逆波兰表达式求值

  3. 思考:为什么 priority_queue(优先级队列)的底层默认是 vector,而不是 deque?

🔔 下一篇预告:我们将学习 priority_queue 优先级队列的使用和模拟实现,还有仿函数的概念!敬请期待~


如果这篇文章对你有帮助,别忘了点赞、收藏、关注哦!有问题欢迎在评论区留言,我会一一解答~

相关推荐
huohaiyu1 小时前
深入解析Java垃圾回收机制
java·开发语言·算法·gc
汉克老师1 小时前
GESP6级C++考试语法知识(五十三、动态规划----背包问题(六、分组背包)
c++·动态规划·背包问题·gesp6级·gesp六级·分组背
YsyaaabB1 小时前
LangChain作业二---多语言翻译Prompt
开发语言·python·langchain
SunnyDays10111 小时前
如何在 Java 中实现 OFD 与 PDF 格式互转
java·开发语言
keykey6.1 小时前
用 PyTorch 训练图像分类器:完整实战
开发语言·人工智能·深度学习·机器学习
雪度娃娃1 小时前
转向现代C++——保证const成员函数的线程安全性
开发语言·c++
坚果派·白晓明1 小时前
[鸿蒙PC三方库移植适配] 使用 AtomCode + Skills 自动完成Protobuf鸿蒙化适配
c语言·c++·华为·harmonyos
原来是猿2 小时前
深入理解 C++ unordered_map 与 unordered_set
开发语言·c++
满天星83035772 小时前
【Qt】信号和槽 (一)(概述和基本使用)
开发语言·c++·qt