首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >深度学习C++中的数据结构:栈和队列

深度学习C++中的数据结构:栈和队列

原创
作者头像
IFMaxue
发布2025-08-28 20:30:51
发布2025-08-28 20:30:51
3830
举报
文章被收录于专栏:学习学习

栈(Stack)与队列(Queue):从基础到实战

在C++中,栈和队列是两种非常重要的容器适配器——它们不直接存储数据,而是基于其他基础容器(如vector、list、deque)实现特定的操作逻辑。今天我们就从定义、使用到底层原理,再到实战例题,手把手带你搞懂这两种数据结构。

一、栈(Stack):先进后出的"叠盘子"

栈的核心特点是先进后出(LIFO,Last In First Out),就像叠盘子一样:最后放上去的盘子,要先拿下来;最底下的盘子,只能等上面的都拿完才能取。

image.png
image.png

1. 栈的定义方式

栈有两种常见定义方式,核心是指定存储的数据类型底层容器(默认用deque)。

定义方式

代码示例

说明

普通定义(默认底层容器)

stack<int> st1;

存储int类型,底层默认用deque容器

指定底层容器

stack<int, vector<int>> st2; stack<int, list<int>> st3;

分别指定vector、list作为底层容器

注意:如果不指定底层容器,C++标准库会默认用deque——因为deque兼顾了vector的随机访问和list的高效插入删除,适合栈的操作场景。

2. 栈的常用操作

栈的操作很简洁,核心围绕"栈顶"(毕竟只能从栈顶进出),常用成员函数如下:

成员函数

功能

示例

empty()

判断栈是否为空,返回bool值

if (st.empty()) { ... }

size()

获取栈中有效元素的个数,返回整数

cout << st.size();

top()

获取栈顶元素(不删除),注意栈空时调用会报错

int val = st.top();

push(x)

将元素x压入栈顶

st.push(10);

pop()

删除栈顶元素(不返回),栈空时调用会报错

st.pop();

swap(st)

交换两个栈的所有元素

st1.swap(st2);

3. 栈的使用示例

下面代码演示了栈的基本操作:压入4个元素,然后从栈顶依次弹出并打印。

代码语言:cpp
复制
#include <iostream>
#include <stack>  // 包含stack头文件
#include <vector> // 若指定vector为底层容器,需包含此头文件

using namespace std;

int main() {
    // 定义一个以vector为底层容器的栈,存储int类型
    stack<int, vector<int>> st;

    // 压入元素(栈顶依次变成1→2→3→4)
    st.push(1);
    st.push(2);
    st.push(3);
    st.push(4);

    // 打印栈的大小(此时有4个元素)
    cout << "栈的大小:" << st.size() << endl; // 输出:4

    // 循环弹出栈顶元素(直到栈空)
    while (!st.empty()) {
        // 先获取栈顶元素并打印
        cout << st.top() << " "; 
        // 再删除栈顶元素(注意:pop()不返回元素,必须先top()再pop())
        st.pop(); 
    }
    cout << endl; // 输出:4 3 2 1(符合先进后出)

    return 0;
}

二、队列(Queue):先进先出的"排队"

队列的核心特点是先进先出(FIFO,First In First Out),就像日常生活中排队买东西:先排队的人先结账,后排队的人只能等前面的人结束才能轮到。

image.png
image.png

1. 队列的定义方式

和栈类似,队列也需要指定数据类型底层容器(默认同样用deque)。

定义方式

代码示例

说明

普通定义(默认底层容器)

queue<int> q1;

存储int类型,底层默认用deque

指定底层容器

queue<int, vector<int>> q2; queue<int, list<int>> q3;

分别指定vector、list作为底层容器

注意:虽然可以用vector作为底层容器,但队列的pop()操作需要删除队头元素——vector删除队头元素效率很低(要移动所有元素),所以实际中更推荐用list或默认的deque。

2. 队列的常用操作

队列的操作围绕"队头"(出队)和"队尾"(入队),常用成员函数如下:

成员函数

功能

示例

empty()

判断队列是否为空,返回bool值

if (q.empty()) { ... }

size()

获取队列中有效元素的个数

cout << q.size();

front()

获取队头元素(不删除),队列空时调用报错

int val = q.front();

back()

获取队尾元素(不删除),队列空时调用报错

int val = q.back();

push(x)

将元素x插入队尾

q.push(10);

pop()

删除队头元素(不返回),队列空时调用报错

q.pop();

swap(q)

交换两个队列的所有元素

q1.swap(q2);

3. 队列的使用示例

下面代码演示了队列的基本操作:插入4个元素,然后从队头依次弹出并打印。

代码语言:cpp
复制
#include <iostream>
#include <queue>  // 包含queue头文件
#include <list>   // 若指定list为底层容器,需包含此头文件

using namespace std;

int main() {
    // 定义一个以list为底层容器的队列,存储int类型
    queue<int, list<int>> q;

    // 插入元素(队尾依次加入1→2→3→4,队头始终是1)
    q.push(1);
    q.push(2);
    q.push(3);
    q.push(4);

    // 打印队列的大小(此时有4个元素)
    cout << "队列的大小:" << q.size() << endl; // 输出:4

    // 循环弹出队头元素(直到队空)
    while (!q.empty()) {
        // 先获取队头元素并打印
        cout << q.front() << " "; 
        // 再删除队头元素
        q.pop(); 
    }
    cout << endl; // 输出:1 2 3 4(符合先进先出)

    return 0;
}

三、实战:用栈解决逆波兰表达式求值

栈的一个经典应用是计算逆波兰表达式(也叫后缀表达式)。先搞懂什么是中缀、后缀表达式:

1. 中缀表达式 vs 后缀表达式

  • 中缀表达式:我们平时写的1 + 2 * 3,操作符在两个操作数中间,需要考虑优先级(先乘后加);
  • 后缀表达式:把操作符放在两个操作数后面,比如1 2 3 * +,不需要考虑优先级,按顺序计算即可。
image.png
image.png

后缀表达式的计算规则很简单:遇到操作数就入栈,遇到操作符就弹出栈顶两个元素,计算后将结果再入栈,最终栈里剩下的就是结果。

比如计算1 2 3 * +

  1. 1、2、3依次入栈 → 栈:1, 2, 3;
  2. 遇到*,弹出3(右操作数)和2(左操作数),计算2*3=6,6入栈 → 栈:1, 6;
  3. 遇到+,弹出6和1,计算1+6=7,7入栈 → 栈:7;
  4. 最终结果就是7。
image.png
image.png

2. 力扣例题:逆波兰表达式求值

题目链接:150. 逆波兰表达式求值 - 力扣(LeetCode)

题目描述

给你一个字符串数组tokens,表示一个逆波兰表达式,返回该表达式的计算结果。操作符只有+-*/,且所有操作数都是整数。

解题思路

完全遵循后缀表达式的计算规则,用栈实现:

  1. 遍历字符串数组tokens
  2. 若当前字符串是操作数(不是+-*/),转成整数后入栈;
  3. 若当前字符串是操作符,弹出栈顶两个元素(注意:先弹的是右操作数,后弹的是左操作数);
  4. 计算两个元素的结果,将结果入栈;
  5. 遍历结束后,栈中仅剩的一个元素就是最终结果。
image.png
image.png
代码实现
代码语言:cpp
复制
#include <iostream>
#include <vector>
#include <stack>
#include <string>

using namespace std;

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        // 1. 定义一个栈,存储操作数和中间结果
        stack<int> st;

        // 2. 遍历字符串数组
        for (auto& str : tokens) {
            // 2.1 判断当前字符串是否为操作符
            if (str == "+" || str == "-" || str == "*" || str == "/") {
                // 弹出栈顶两个元素(注意顺序:先弹右操作数,后弹左操作数)
                int right = st.top(); // 右操作数(后入栈的)
                st.pop();
                int left = st.top();  // 左操作数(先入栈的)
                st.pop();

                // 根据操作符计算结果,结果入栈
                switch (str[0]) { // str是单个字符的字符串,取第一个字符判断
                    case '+':
                        st.push(left + right);
                        break;
                    case '-':
                        st.push(left - right); // 注意:左减右,不是右减左
                        break;
                    case '*':
                        st.push(left * right);
                        break;
                    case '/':
                        st.push(left / right); // 注意:左除右,且题目保证是整数除法
                        break;
                }
            } else {
                // 2.2 是操作数,转成整数后入栈(stoi:string to int)
                st.push(stoi(str));
            }
        }

        // 3. 遍历结束,栈中仅剩的元素就是结果
        return st.top();
    }
};

// 测试代码
int main() {
    Solution sol;
    // 示例:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
    vector<string> tokens = {"10","6","9","3","+","-11","*","/","*","17","+","5","+"};
    cout << "逆波兰表达式结果:" << sol.evalRPN(tokens) << endl; // 输出:22
    return 0;
}

四、底层原理:为什么栈和队列是"容器适配器"?

前面我们反复提到"容器适配器",到底什么是适配器?

简单说:适配器是一种"包装器",它基于已有的容器(如vector、list、deque),通过限制部分操作、封装特定接口,实现新的数据结构功能

栈和队列本身不存储数据,而是"借用"底层容器的存储空间,只暴露符合自身逻辑的接口(比如栈只暴露栈顶操作,队列只暴露队头/队尾操作)。

image.png
image.png

1. 栈的底层实现逻辑

栈的核心是"先进后出",所以无论用哪种底层容器,只需确保所有操作都针对容器的"一端"(比如vector的尾端、list的尾端):

  • push():调用底层容器的push_back()(在尾端插入);
  • pop():调用底层容器的pop_back()(删除尾端元素);
  • top():调用底层容器的back()(获取尾端元素)。
image.png
image.png
image.png
image.png

2. 队列的底层实现逻辑

队列的核心是"先进先出",所以需要确保入队在一端,出队在另一端

  • push():调用底层容器的push_back()(队尾插入);
  • pop():调用底层容器的pop_front()(队头删除);
  • front():调用底层容器的front()(获取队头);
  • back():调用底层容器的back()(获取队尾)。

这也是为什么不推荐用vector作为队列的底层容器——vector没有pop_front()接口(删除队头元素需要移动所有元素,效率低),而list和deque都有高效的pop_front()

image.png
image.png
image.png
image.png

3. 容器适配器的设计思想

容器适配器的设计体现了C++的"复用"思想:不需要重新开发存储逻辑,而是基于已有的高效容器,通过封装接口实现新的数据结构。

除了栈和队列,C++中还有priority_queue(优先队列)也是容器适配器,底层默认用vector,通过堆排序实现"每次出队最大/最小元素"的逻辑。

image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png

总结

  1. :先进后出,操作围绕栈顶,默认底层容器是deque;
  2. 队列:先进先出,操作围绕队头/队尾,默认底层容器是deque;
  3. 核心应用:栈适合解决"后入先出"场景(如逆波兰表达式、括号匹配),队列适合解决"先入先出"场景(如任务调度、广度优先搜索);
  4. 底层本质:容器适配器,基于现有容器封装接口,复用存储逻辑。

掌握栈和队列的核心逻辑后,很多算法题都会变得简单——比如括号匹配、滑动窗口最大值、二叉树的层序遍历等,后续可以继续深入练习这些场景~

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 栈(Stack)与队列(Queue):从基础到实战
    • 一、栈(Stack):先进后出的"叠盘子"
      • 1. 栈的定义方式
      • 2. 栈的常用操作
      • 3. 栈的使用示例
    • 二、队列(Queue):先进先出的"排队"
      • 1. 队列的定义方式
      • 2. 队列的常用操作
      • 3. 队列的使用示例
    • 三、实战:用栈解决逆波兰表达式求值
      • 1. 中缀表达式 vs 后缀表达式
      • 2. 力扣例题:逆波兰表达式求值
    • 四、底层原理:为什么栈和队列是"容器适配器"?
      • 1. 栈的底层实现逻辑
      • 2. 队列的底层实现逻辑
      • 3. 容器适配器的设计思想
    • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档