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

《C++ STL容器适配器:stack和queue的实现机制与应用场景》

目录

引言

一、容器适配器的本质

1.1 设计哲学

1.2 底层容器选择策略

二、Stack的工程实践

2.1 核心操作与内存模型

2.2 典型应用场景

场景1:括号匹配验证

场景2:函数调用栈模拟

练手算法题:

155. 最小栈 - 力扣(LeetCode)

栈的压入、弹出序列_牛客题霸_牛客网

150. 逆波兰表达式求值 - 力扣(LeetCode) 后缀表达式

1. 快速判断运算符

2. 性能分析

224. 基本计算器 - 力扣(LeetCode)(给一个中缀计算结果)拓展题目

三、队列

3.1 创建队列对象

3.2 六大核心方法

队列的三大典型应用场景

3.3 消息队列实现

3.4 广度优先搜索(BFS)

3.5 实时数据缓冲

栈的模拟实现:

队列的模拟实现:

​编辑

deque实现思想:

deque优缺点


让我们先来看看库中的stack和queue

引言

在C++标准模板库(STL)中,stack和queue作为典型的容器适配器,是每位开发者必须掌握的核心数据结构。它们看似简单,却蕴含着程序设计中的重要哲学——通过限制操作来实现特定的数据管理策略。本文将从底层实现原理出发,结合工程实践中的典型应用场景,揭示这两种线性容器的本质特性。

一、容器适配器的本质

1.1 设计哲学

stack和queue并非独立容器,而是构建在其他序列容器(如deque、list)之上的适配器。这种设计体现了软件工程中的"组合优于继承"原则,通过限制基础容器的接口来实现特定的访问策略。

template <class T, class Container = deque<T>> 
class stack;

template <class T, class Container = deque<T>>
class queue;

1.2 底层容器选择策略

  • stack默认使用deque:支持快速的首尾插入/删除操作(O(1)时间复杂度)

  • queue默认使用deque:同时需要高效的头部删除和尾部插入

  • 可替换容器类型验证:

    • vector(仅适用于stack)

    • list(适用于两者)

二、Stack的工程实践

2.1 核心操作与内存模型

stack<int> s;
s.push(1);      // 压栈 O(1)
s.emplace(2);   // 原地构造 C++11
s.top();        // 查看栈顶 O(1)
s.pop();        // 出栈 O(1)

内存增长示意图:

[栈底] -> 元素1 -> 元素2 -> ... -> 元素N [栈顶]

2.2 典型应用场景

场景1:括号匹配验证

解题思路是:

栈的使用:栈的“后进先出”特性完美匹配括号的嵌套关系。例如,遇到左括号时压栈,遇到右括号时检查栈顶是否匹配。

#include <stack>
using namespace std;

bool isValidParentheses(const string& s) {
    stack<char> stk;
    for (char c : s) {
        if (c == ')' || c == ']' || c == '}') { // 当前字符是右括号
            if (stk.empty()) return false;     // 栈为空,无法匹配
            // 根据右括号类型检查栈顶是否匹配
            if ((c == ')' && stk.top() != '(') ||
                (c == ']' && stk.top() != '[') ||
                (c == '}' && stk.top() != '{')) {
                return false;
            }
            stk.pop(); // 匹配成功,弹出栈顶左括号
        } else {
            stk.push(c); // 左括号直接入栈
        }
    }
    return stk.empty();
}
场景2:函数调用栈模拟
struct FunctionCall {
    int returnAddress;
    vector<int> parameters;
};

stack<FunctionCall> callStack;

// 函数调用
callStack.push({0x0040A2B0, {1, 2, 3}});

// 函数返回
FunctionCall ret = callStack.top();
callStack.pop();

练手算法题:

155. 最小栈 - 力扣(LeetCode)

解题思路:

提供两个栈,一个栈用来存原数_st,一个栈用来存小值_minst

_minst:在空栈的时候先和_st一样存入一个值,后续入栈时都需要与_minst栈顶比较,

入栈的值比当前_minst.top()值小或等于则继续入这个_minst栈。

代码实现:

class MinStack {
public:
    MinStack() {
                
    }
    
    void push(int val) {
        _st.push(val);
        if(_minst.empty() || _minst.top() >= val)
        {
            _minst.push(val);
        }
    }
    
    void pop() {
        if(_minst.top() == _st.top())
        {
            _minst.pop();
        }
        _st.pop();
    }
    
    int top() {
        return _st.top();
    }
    
    int getMin() {
        return _minst.top();
    }

    stack<int> _st;
    stack<int> _minst;
};

栈的压入、弹出序列_牛客题霸_牛客网

解题思路:

1.先按入栈序列入栈

2.栈顶元素和出栈序列是否匹配

        a.如果匹配,则出数据,直到不匹配或栈为空

        b.如果不匹配,则继续入数据,直到匹配

3.结束标志:入栈序列走完了

代码实现:

    bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
        // write code here
        int pushi = 0, popi = 0;
        stack<int> st;
        while(pushi < pushV.size())
        {
            st.push(pushV[pushi]);//st.push(pushV[pushi++]);下面的++pushi就不写了
            //栈不为空且栈顶元素和出栈序列匹配
            while(!st.empty()&&st.top() == popV[popi])
            {
                st.pop();
                ++popi;
            }
            ++pushi;
        }
        
        return st.empty();
    }

150. 逆波兰表达式求值 - 力扣(LeetCode) 后缀表达式

理解题意:

eg1:

中缀表达式:a + b * (c - d)

改成后缀表达式: abcd - * + 

eg2:

中缀表达式:a + b * c - d

后缀表达式:abc * + d - 

巧妙解法:操作数按顺序排列,再看中缀,每当遇见操作符就往前看操作数根据符号优先级能否直接运算,能,则填入运算符,不能则获取下一个操作数,再看前面已经有的操作数和已有的运算符能否计算。比如现将abcd写出,再看中缀,发现a + b不能直接运算,继续往下看,录入*,不能运算,再录入c ,b*c符合逻辑,写上a(bc*),前面的+,也可以运算,写上(a(bc*)+)继续往下看 ,只有-d,于是:(a(bc*)+)d-写出,去掉括号,abc*+d-

解题思路:

题目是给出后缀表达式,实际上我们可以按照就近(一个符号和两个值就可以组成一次运算,先取出来的值是右操作数,后取到的值是)倒推,比如说:

ab*c/d+ <--->((a*b)/c)+d <-->ab*c/d+

两个值加一个符号就能组成一次运算,得到一个运算结果,相当于一个新值,再获取一个值一个符号这样的顺序。

1.依次将对象中的字符入栈,遇到操作符就出栈,依次为右操作数和左操作数,再将运算结果入栈将成为下一个运算的左操作数。

2.最后一个在栈中得数就是总运算结果。

方法一:暴力解法

    class Solution {
        public:
              int evalRPN(vector<string>& tokens) {
              stack<int> s;
              for (size_t i = 0; i < tokens.size(); ++i)
               {
               string& str = tokens[i];
               // str为数字
               if (!("+" == str || "-" == str || "*" == str || "/" == str))
               {
                s.push(atoi(str.c_str()));
               }
                else
               {
                 // 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 '/':
                    // 题目说明了不存在除数为0的情况
                    s.push(left / right);
                    break;
                }
            }
        }
 
        return s.top();
    }
 };

 方法二:用set(底层是搜索树)来解 

1. 快速判断运算符

  • 核心目的:将运算符集合 (+-*/) 存储在一个 set 中,用于快速检查当前字符串是否为运算符。这里考虑到的是如果在符号很多的情况下,优先使用set能便于快速查找。

  • 实现方式

    
    if (s.find(str) != s.end()) {  // 判断 str 是否在集合 s 中
        // 执行运算符操作
    } else {
        // 处理操作数
    }
  • 2. 性能分析

  • 时间复杂度set 的查找操作 find() 的时间复杂度为 O(log n),其中 n 是集合大小(此处 n=4,实际几乎可以视为常数时间)。

  • 对比线性查找:对于少量元素(如4个运算符),set 的查找效率与逐个条件判断 (str == "+" || ...) 差异不大,但代码更简洁。

  • set 在这段代码中充当了一个运算符过滤器,通过预定义的运算符集合,高效区分当前 token 是操作符还是操作数。这种设计在代码简洁性可维护性之间取得了平衡,尤其适合需要灵活扩展运算符的场景。

代码实现:

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        //initializer_list构造函数
        set<string> s = {"+","-","*","/"};
        for(auto str : tokens) 
        {
            //1.操作数入栈,操作符运算
            if(s.find(str) != s.end())
            {
                //操作符
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();

                switch(str[0])//case必须是整型(char也是整型)
                {
                    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
            {
                //没有找到操作符,就入栈
                st.push(stoi(str));//string转化为int类型
            }

        }
        return st.top();
    }
};

224. 基本计算器 - 力扣(LeetCode)(给一个中缀计算结果)拓展题目

解题思路:中缀转后缀

1.操作数输出

2.操作符入栈:

        a、栈为空,入栈

        b、比栈顶的运算符优先级高,就先入栈

        c、比栈顶的运算符优先级低,出栈顶运算符

        d、优先级相等,前一个可以先运算,出栈顶运算符

结束后将栈中操作符全部出栈。

转成后缀。

        只要遇到左括号就递归,遇到右括号结束。(递归时会建立新栈,新的一轮优先级的比较)。

代码:省略;

三、队列

3.1 创建队列对象

#include <queue>

// 创建整型队列
queue<int> myQueue;

// 使用其他容器作为底层实现
queue<string, list<string>> listBasedQueue;

3.2 六大核心方法

  1. 入队操作

myQueue.push(42);
myQueue.emplace("Hello"); // C++11高效构造
  1. 访问元素

cout << "队首元素: " << myQueue.front();
cout << "队尾元素: " << myQueue.back(); // 注意:非标准队列可能不支持
  1. 出队操作

myQueue.pop(); // 删除队首元素
  1. 容量查询

if (!myQueue.empty()) {
    cout << "当前元素数量: " << myQueue.size();
}

队列的三大典型应用场景

3.3 消息队列实现

class MessageQueue {
private:
    queue<string> messages;
    mutex mtx;

public:
    void addMessage(const string& msg) {
        lock_guard<mutex> lock(mtx);
        messages.push(msg);
    }

    string getMessage() {
        lock_guard<mutex> lock(mtx);
        if (messages.empty()) return "";
        
        string msg = messages.front();
        messages.pop();
        return msg;
    }
};

应用场景:多线程通信、事件处理系统

3.4 广度优先搜索(BFS)

void BFS(vector<vector<int>>& graph, int start) {
    vector<bool> visited(graph.size(), false);
    queue<int> q;

    q.push(start);
    visited[start] = true;

    while (!q.empty()) {
        int current = q.front();
        q.pop();
        
        // 处理当前节点
        cout << "访问节点: " << current << endl;

        for (int neighbor : graph[current]) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                q.push(neighbor);
            }
        }
    }
}

应用场景:社交网络关系分析、路径规划

3.5 实时数据缓冲

class DataBuffer {
    queue<SensorData> buffer;
    const int MAX_SIZE = 100;

public:
    void addData(const SensorData& data) {
        if (buffer.size() >= MAX_SIZE) {
            buffer.pop(); // 移除最旧数据
        }
        buffer.push(data);
    }

    void processBatch() {
        while (!buffer.empty()) {
            process(buffer.front());
            buffer.pop();
        }
    }
};

应用场景:物联网传感器数据处理、音视频流缓冲

栈的模拟实现:

#pragma once
#include<vector>
#include<list>
namespace bit
{
    //写法一
	//template<class T>
	//class stack
	//{
	//public:
	//private:
	//	//1.数组
	//	T* _a;
	//	//2.top
	//	int _top;
	//	//3.容量
	//	int _capacity;
	//};

    // 写法二:
	//设计模式:适配器模式  -- 转换
	//我们可以用vector、list来实现栈,因此也可以做一个容器模版,我们不会知道我们的栈是数组栈还是链表栈

	//泛型编程
	//stack<int, vector<int>> st1
	//stack<int, list<int>> st2
	template<class T, class Container>
	class stack
	{
	public:
		//1 2 3 4 5
		void push(const T& x)
		{
			_con.push_back(x);
		}

		void pop()
		{
			_con.pop_back();
		}

		size_t size()
		{
			return _con.size();
		}

		bool empty()
		{
			return _con.empty();
		}

		const T& top()
		{
			return _con.back();
		}

	private:
		Container _con;

	};

测试代码:

void test_stack1()
{

	bit::stack<int, vector<int>> st;

	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);

	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	cout << endl;
}

int main()
{
	test_stack1();
}

在我们实际应用时,stack的第二个参数是可以不传的,因此我们的模拟代码还需要修改。

我们的模版参数和函数参数相似,模版参数传的是类型,函数参数传的是对象,函数参数可以有缺省参数,模版参数也可以有缺省参数,从右往左缺省。

如图:写一个默认参数

队列的模拟实现:

和栈的模拟实现类似,有一些区别,因为队列是先进先出,因此在pop()时,应该pop_front(); 

以及在库中对于队列模版参数默认值是传的deque(双端队列),不是真队列(不要求先进先出)

deque是一个很牛的容器

vector

优点:支持下标随机访问

缺点:头部或者中间插入的效率低,扩容有消耗

list

优点:任意位置插入删除效率都不错

缺点:不支持下表随机访问

但如上图:我们可以发现,deque就是vector和list的合体

deque实现思想:

1.开多个小数组

2.中控指针数组 指针从中间往两边放,存小数组地址

尾差:最后一个buffer没满,就插入这个buffer里面,如果满了就新开一个buffer,在中控指针数组内再往后添加一个指针,指向新的数组地址

头插: 在中控指针数组内再往前添加一个指针指向新数组空间,在新的数组空间内头插(请注意:指针存的是数组地址,存入数据是从数组尾部开始存入)

中控数组满:扩容(类似于vector扩容的方式)

扩容后,要找第i个值需要做两个判断:

1.如果第一个buffer不满:   i -= 第一个buffer的数据个数。

(   如果是满的,就不用i -= 第一个buffer的数据个数。)

2.再算在第几个buffer里面:buff -> i/N;在这个buffer内的第几个? i%N

总图:

deque优缺点

优点:头插尾插的效率都很好。比顺序表和链表都要好一些

缺点:

1.中间插入删除会比较麻烦,效率一般。a.整体移动,b.局部移动

2.[]效率不够极致

为什么栈和队列的默认容器都是deque?因为deque的头尾插的效率很好。

队列的模拟实现代码:

#pragma once
#include<deque>
#include<list>
namespace bit
{

	template<class T, class Container = deque<T>>
	class Queue
	{
	public:
		//1 2 3 4 5
		void push(const T& x)
		{
			_con.push_back(x);
		}

		void pop()
		{
			_con.pop_front(); //注意在队头出
		}

		size_t size()
		{
			return _con.size();
		}

		bool empty()
		{
			return _con.empty();
		}

		const T& front()
		{
			return _con.front();
		}

		const T& back()
		{
			return _con.back();
		}

	private:
		Container _con;

	};



}

结语:

       随着这篇关于题目解析的博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。

       在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。

        你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注,这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。

相关文章:

  • nvm list available为空
  • K8S学习之基础十九:k8s的四层代理Service
  • Python - 轻量级后端框架 Flask
  • PH|EH————meta
  • python使用django搭建图书管理系统
  • Android Retrofit + RxJava + OkHttp 网络请求高效封装方案
  • 并查集模板
  • 29-验证回文串
  • 【C++初阶】类与对象(下)
  • Docker 运行 GPUStack 的详细教程
  • 蓝桥杯刷题周计划(第二周)
  • Scala 中trait的线性化规则(Linearization Rule)和 super 的调用行为
  • GC安全点导致停顿时间过长的案例
  • 深入解析跨域问题及其解决方案:从原理到代码实践
  • (安全防御)旁挂组网双机热备负载分担实验
  • coding ability 展开第二幕(双指针——巩固篇)超详细!!!!
  • Codeforces Round 976 (Div. 2) (部分题解)
  • webtinyserver讲解
  • TypeScript系列06-模块系统与命名空间
  • 《Linux栈破坏了,如何还原》
  • 简洁的网站案例/宁波seo教程行业推广
  • 家装设计网站开发/怎么做公司网页
  • 深圳 做网站 车公庙/营销推广方案包括哪些内容
  • 有做网站维护的/网站服务公司
  • 定制网站开发/郑州短视频代运营公司
  • 类似站酷的设计类网站/无代码免费web开发平台