【C++】stack、queue和deque

news2024/11/27 19:51:48

​🌠 作者:@阿亮joy.
🎆专栏:《吃透西嘎嘎》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在这里插入图片描述

目录

    • 👉stack 的介绍和使用👈
      • stack 的介绍
      • stack 的使用
        • 1. 最小栈
        • 2. 验证栈序列
        • 3. 逆波兰表达式求值
        • 4. 中缀表达式转为后缀表达式
        • 5. 根据中缀表达式构建二叉树
        • 6. 中缀表达式求值
        • 7. 中缀表达式转化为前缀表达式
    • 👉queue 的介绍和使用👈
      • queue 的介绍
      • queue 的使用
    • 👉stack 的模拟实现👈
    • 👉queue 的模拟实现👈
    • 👉容器适配器👈
      • 什么是适配器
      • STL标准库中 stack 和 queue 的底层结构
      • deque
        • 1. deque 的原理介绍
        • 2. deque 的缺陷
    • 👉总结👈

👉stack 的介绍和使用👈

stack 的介绍

  1. stack 是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
  2. stack 是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
  3. stack 的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
    • empty:判空操作
    • back:获取尾部元素操作
    • push_back:尾部插入元素操作
    • pop_back:尾部删除元素操作
  4. 标准容器 vector、deque、list 均符合这些需求,默认情况下,如果没有为 stack 指定特定的底层容器,默认情况下使用 deque。

在这里插入图片描述
在这里插入图片描述

stack 的使用

函数说明接口说明
stack()构造空的栈
empty()检测stack是否为空
size()返回stack中元素的个数
top()返回栈顶元素的引用
push()将元素val压入stack中
pop()将stack中尾部的元素弹出

1. 最小栈

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

  • MinStack() 初始化堆栈对象。
  • void push(int val) 将元素val推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素。
  • int getMin() 获取堆栈中的最小元素。

在这里插入图片描述

思路:最小栈MinStack包含两个栈:一个是存储插入元素的栈_st,另一个是存储最小值的栈_minst。插入元素时,必须向栈_st中插入新元素。对于_minst来说,如果栈_minst为空,直接向_minst插入新元素。如果栈_minst不为空,当新插入的元素小于或等于_minst的栈顶元素,那么向_minst中插入新元素;而新插入的元素大于_minst的栈顶元素,则向_minst中插入栈顶元素。那么,此时的_minst中的栈顶元素就是栈_st对应位置的最小值。出栈时,让栈_st_minst同时出栈即可。

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

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

在这里插入图片描述

其实按照上面的思路来解题,栈_minst中可能会存储着大量的重复的最小值。那么我们可以做一些小小的优化:当新插入元素大于_minst栈顶元素时,不再向_minst插入栈顶元素了。按照这种思路的话,出栈也要做相应的改变:当_st栈顶元素等于_minst栈顶元素时,_minst才会出栈。

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

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

在这里插入图片描述

但是第二种思路面对着大量重复的最小值的场景时,也会存在空间浪费的情况。那么我们还可以进一步的优化,增加多一个计算器并将其封装成一个类struct valCont,该类的成员变量_val_count。此时,栈_minst的元素是struct valCount。当新插入元素等于_minst.top()._val时,++_minst.top()._count。当_minst.top()._count == 0时,则_minst栈顶元素出栈。

struct valCount
{
    int _val;
    int _count;

    valCount(int val = 0, int count = 0)
    {
        _val = val;
        _count = count;
    }
};

class MinStack 
{
public:
    MinStack() {}
    
    void push(int val) 
    {
        _st.push(val);
        if(_minst.empty() || val < _minst.top()._val)
        {
            _minst.push(valCount(val, 1));
        }
        else if(val == _minst.top()._val)
        {
            ++_minst.top()._count;
        }
    }
    
    void pop() 
    {
        if(_st.top() == _minst.top()._val)
        {
            --_minst.top()._count;
            if(_minst.top()._count == 0)
                _minst.pop();
        }        
        _st.pop();
    }
    
    int top() 
    {
        return _st.top();
    }
    
    int getMin() 
    {
        return _minst.top()._val;
    }

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

注:struct valCount 最好自己提供默认构造函数,这样就可以支持匿名对象的用法了。以上思路实现的 MinStack 都没有提供构造函数,为什么也有过呢?因为对于自定义类型,编译器会调用自定义类型的默认构造函数。

在这里插入图片描述

2. 验证栈序列

给定 pushed 和 popped 两个序列,每个序列中的值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false 。

在这里插入图片描述

思路:给定一个入栈序列,可能会有多个出栈序列。因为会存在边入边出、入几个再出等等情况。本道题是让我们验证出栈序列popped是不是入栈序列pushed的出栈序列。那么如何解决这道题呢?这道题可以这样来解决:1. 入栈序列的元素先入栈 2. 栈顶元素和出栈序列的元素比较,相等则栈顶元素出栈并继续比较 3. 当入栈序列的元素已全部入栈,循环结束。如果栈为空或者出栈序列也全部遍历了一次,那么popped就是pushed的出栈序列了,否则不是。

在这里插入图片描述

class Solution 
{
public:
    bool validateStackSequences(vector<int>& pushed, vector<int>& popped) 
    {
        stack<int> st;
        int i = 0;     // 遍历出栈序列popped用的下标
        for(auto& e : pushed)
        {
            st.push(e);     // 入栈序列先入栈
            // 比较:相同则出栈且继续比较
            while(!st.empty() && st.top() == popped[i])
            {
                st.pop();
                ++i;
            }
        }
        //return i == popped.size();
        return st.empty();
    }
};

在这里插入图片描述

3. 逆波兰表达式求值

给你一个字符串数组 tokens ,表示一个根据逆波兰表示法表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。

注意:

  • 有效的算符为 ‘+’、‘-’、‘*’ 和 ‘/’ 。
  • 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
  • 两个整数之间的乘法总是 向零截断 。
  • 表达式中不含除零运算。
  • 输入是一个根据逆波兰表示法表示的算术表达式。
  • 答案及所有中间计算结果可以用 32 位 整数表示。

在这里插入图片描述

逆波兰表达式是一种后缀表达式,而我们日常学习用到的是中缀表达式。中缀表达式操作符在中间,操作数在两遍;而后缀表达式则操作符在后面,操作数在前面。如:前缀表达式2 + 3 * 4对应的后缀表达式为2 3 4 * +。那为什么要有后缀表达式呢?因为计算机是顺序读入表达式的,当读到操作符时,计算机就会去两个操作数进行运算。如果采用的是中缀表达式,计算机将无法识别该操作符后是否存在更高级的操作符。而后缀表达式不会存在这样的情况,可以依据后缀表达式的次序计算出正确结果。

那么解决这道题的思路是什么呢?其实很简单,就是遇到数字则入栈;遇到操作符则取栈顶两个数组进行计算(对于减法和除法,需区分左操作数和右操作数),并将结果压入栈中。遍历结束,栈顶元素就是后缀表达式的计算结果。

在这里插入图片描述

class Solution 
{
public:
    int evalRPN(vector<string>& tokens) 
    {
        stack<int> st;
        for(auto& str : tokens)
        {
            if(str == "+" || str == "-" || str == "*" || str == "/")
            {
                int right = st.top();   // 右操作数
                st.pop();
                int left = st.top();    // 左操作数
                st.pop();

                switch(str[0])  // 计算并将计算结果压入栈中
                {
                    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));		// 操作数入栈
            }
        }
        return st.top();    // 后缀表达式的结果
    }
};

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

stoi 函数的第二个参数是一个输出型参数,其值为 str 转化成整型数字时的结束位置的下一个位置,该参数可以设置为nullptr,表明不关心该值;第三个参数则表示要转化的字符串 str 里的数字是什么进制的。该参数的缺省值为 10,表示 str 里的数字是十进制的。如果该值被设置为 0 ,则需要根据 str 里的标致来判别其数字是什么进制的。

stoi 函数的演示使用

#include <iostream>
#include <string>     
using namespace std;

int main()
{
	string str_dec = "2001, A Space Odyssey";
	string str_hex = "40c3";
	string str_bin = "-10010110001";
	string str_auto = "0x77";

	std::string::size_type sz;   // alias of size_t

	int i_dec = std::stoi(str_dec, &sz);
	int i_hex = std::stoi(str_hex, nullptr, 16);	// 十六进制
	int i_bin = std::stoi(str_bin, nullptr, 2);		// 2进制
	int i_auto = std::stoi(str_auto, nullptr, 0);	// 

	cout << str_dec << ": " << i_dec << " and [" << str_dec.substr(sz) << "]\n";
	// substr(sz)表示从sz位置取到str_dec的结尾作为子串
	cout << str_hex << ": " << i_hex << '\n';
	cout << str_bin << ": " << i_bin << '\n';
	cout << str_auto << ": " << i_auto << '\n';

	return 0;
}

在这里插入图片描述

4. 中缀表达式转为后缀表达式

注:该中缀表达式中包含 + - * / ( ) =等符号,且包含空格。转换得到的后缀表达式也不包含空格。

那么如何将中缀表达式转为后缀表达式呢?

  • 从左往右遍历中缀表达式,跳过空格
  • 遇到操作数直接输出
  • 如果当前操作符为左括号,左括号直接入栈
  • 如果当前操作符为右括号,让栈顶到左括号之间的操作符出栈添加到后缀表达式中,括号不出现在后缀表达式中
  • 如果当前操作符优先级小于或等于栈顶操作符的优先级,先将栈顶操作符出栈直至栈顶操作符优先级小于当前操作符优先级,再将当前操作符入栈
  • 遍历完中缀表达式,将栈中的操作符出栈添加到后缀表达式中

注意:出栈有可能会出多个操作符;栈为空时,操作符一定要入栈。遍历结束时,栈内的操作符要依次出栈。

class Solution 
{
public:
    vector<string> convertToRPN(vector<string> &expression) 
    {
        vector<string> ret;
        stack<string> st;   // 存储操作符的栈
        for(auto& str : expression)
        {
            if(str == "(")  // 左括号直接入栈
                st.push(str);
            else if(str == ")")
            {
                // 左括号上面的操作符全部出栈添加到后缀表达式中
                while(st.top() != "(")
                {
                    ret.push_back(st.top());
                    st.pop();
                }
                st.pop();   // 左括号出栈但不添加到后缀表达式中
            }
            else if(str == "+" || str == "-" || str == "*" || str == "/")
            {
                // 栈顶操作符优先级大于或等于当前操作符,则出栈
                while(!st.empty() && compare(st.top(), str))
                {
                    ret.push_back(st.top());
                    st.pop();
                }
                st.push(str);   // 当前操作符入栈
            }
            else
            {
                ret.push_back(str);   // 数字直接添加到后缀表达式中
            }
        }
        while(!st.empty())
        {
            ret.push_back(st.top());
            st.pop();
        }
        return ret;
    }
private:
    // 比较操作符的优先级:first >= second时,返回 true
    bool compare(const string& first, const string& second)
    {
        if(first == "*" || first == "/")
            return true;
        else if(first == "(")
            return false;
        else if(second == "+" || second == "-")
            return true;
        else
            return false;
    }
};

在这里插入图片描述

在这里插入图片描述

5. 根据中缀表达式构建二叉树

其实将中缀表达式构建成二叉树的思路差不多,思路如下:

  • 从左往右遍历中缀表达式
  • 遇到操作数时,建立新节点存储该操作数并将该节点压入操作数栈中
  • 当操作符从操作符栈中出栈时,为该操作符新建一个节点,并从操作数栈中 pop 出两个操作数节点,第一个操作数节点作为操作符节点的右孩子,第二个操作数节点作为操作符节点的左孩子,将新节点压入操作数栈中(注:节点 TreeNode 的值是 string,操作数栈存储的是 TreeNode*)。该过程直至操作符栈的栈顶操作符优先级小于当前操作符的优先级,最后将当前操作符压入操作符栈中。
  • 注:操作符优先级关系同将中缀表达式转为后缀表达式的操作符优先级关系
  • 当最后一个操作符出栈时,就构成了二叉表达树,且最后一个操作符节点为根节点
  • 该二叉表达书的前序遍历就是前缀表达式(波兰表达式),中序遍历就是中缀表达式,后序遍历就是后缀表达式(逆波兰表达式)。

有些计算器就是通过将中缀表达式转化成后缀表达式,再计算后缀表达式求出结果。

6. 中缀表达式求值

给一个用字符串表示的中缀表达式数组,求出这个中缀表达式的值。

表达式只包含整数,+,-,*,/,(,)

在这里插入图片描述

思路:

  • 从左往右遍历中缀表达式
  • 遇到操作数,将其放入操作数栈stack<int>
  • 遇到左括号,直接将左括号放入操作符栈stack<string>
  • 遇到右括号,操作符栈的元素出栈直至左括号成为栈顶元素。注意操作符栈元素出栈的过程中,操作数栈也要出两个操作数来进行计算,并将计算结果放回操作数栈中。最后左括号出栈
  • 当遇到+ - * /操作符时,需比较操作符的优先级。如果栈顶操作符优先级高,则进行计算并将计算结果压入操作数栈中。最后弹出栈顶操作符并将当前操作符压入操作符栈中
  • 遍历结束,将操作符栈和操作数栈中的元素进行计算。最后操作数栈的栈顶元素就是中缀表达式的计算结果
class Solution 
{
public:
    int evaluateExpression(vector<string> &expression) 
    {
        if(expression.size() == 0)
            return 0;
        stack<int> st1;  // 操作数栈
        stack<string> st2;  // 操作符栈
        for(auto& str : expression)    // 遍历中缀表达式
        {
            if(str == "(")  // 左括号直接入栈
                st2.push(str);
            else if(str == ")")
            {
                while(st2.top() != "(")
                {
                    calc(st1, st2);    // 操作符边出栈边计算
                    st2.pop();
                }
                st2.pop();  // 左括号出栈
            }
            else if(str == "+" || str == "-" || str == "*" || str == "/")
            {
                while(!st2.empty() && compare(st2.top(), str))
                {
                    calc(st1, st2);
                    st2.pop();
                }
                st2.push(str);
            }
            else
                st1.push(stoi(str));    // 字符串转为操作数
        }
        // 将两个栈的剩余元素进行计算
        while(!st2.empty())
        {
            calc(st1, st2);
            st2.pop();
        }
        
        return st1.top();
    }
private:
    // 比较操作符优先级
    bool compare(const string& first, const string& second)
    {
        if(first == "(")    // 栈顶操作符为左括号,当前操作符直接入栈
            return false;
        else if(first == "*" || first == "/")
            return true;
        else if(second == "+" || second == "-")
            return true;
        else 
            return false;
    }
    // 计算
    void calc(stack<int>& st1, stack<string>& st2)
    {
        int right = st1.top();
        st1.pop();
        int left = st1.top();
        st1.pop();
        switch(st2.top()[0])
        {
            case '+':
                st1.push(left + right);
                break;
            case '-':
                st1.push(left - right);
                break;
            case '*':
                st1.push(left * right);
                break;
            case '/':
                st1.push(left / right);
                break;
        }
    }
};

在这里插入图片描述

7. 中缀表达式转化为前缀表达式

给定一个字符串数组,它代表一个表达式,返回该表达式的波兰表达式(去掉括号)。

在这里插入图片描述

思路:

  • 从右向左遍历中缀表达式
  • 遇到数字,直接添加到vector<string> ret的末尾
  • 遇到右括号,入操作符栈stack<string> st
  • 遇到左括号,操作符栈元素出栈并添加到ret的末尾中,直至右括号弹出(右括号不添加到ret中)
  • 如果遇到操作符,操作符栈弹栈至栈顶操作符不大于当前操作符,所有弹出的操作符依次添加到ret的末尾,最后再将该操作符入栈
  • 出于方便,我们将所有操作符的优先级设置为:*/最高,+-次之,然后是右括号,最后是左括号
  • 遍历完后,将操作符栈中的元素依次弹出并添加到ret的末尾。最后将ret反转就能得到前置表达式了
#include <iostream>
#include <vector>
#include <string>
#include <stack>
using namespace std;

class Solution
{
public:
	vector<string> Convertion(vector<string>& expression)
	{
		vector<string> ret;
		stack<string> st;
		for (int i = expression.size() - 1; i >= 0; --i)
		{
			string& str = expression[i];
			if (str == ")")    // 右括号直接入栈
				st.push(str);
			else if (str == "(")
			{
				while (st.top() != ")")
				{
					ret.push_back(st.top());
					st.pop();
				}
				st.pop();	// 右括号出栈
			}
			else if (str == "+" || str == "-" || str == "*" || str == "/")
			{
				// 栈顶操作符优先级大于当前操作符优先级,则出栈
				while (!st.empty() && getPriority(st.top()) > getPriority(str))
				{
					ret.push_back(st.top());
					st.pop();
				}
				st.push(str);
			}
			else    // 操作数则直接添加到ret的末尾
				ret.push_back(str);
		}
		// 栈中的操作符全部出栈
		while (!st.empty())
		{
			ret.push_back(st.top());
			st.pop();
		}
		reverse(ret.begin(), ret.end());	// 将ret反转得到前缀表达式
		return ret;
	}

private:
	int getPriority(const string& str)
	{
		if (str == "*" || str == "/")
			return 3;
		else if (str == "+" || str == "-")
			return 2;
		else if (str == ")")
			return 1;
		else
			return 0;
	}
};

int main()
{
	vector<string> expression1 = { "(", "5", "-", "6", ")", "*", "7" };
	vector<string> expression2 = { "3", "+", "(", "1", "-", "2", ")" };
	vector<string> ret1 = Solution().Convertion(expression1);
	vector<string> ret2 = Solution().Convertion(expression2);

	for (auto& str : ret1)
	{
		cout << str << " ";
	}
	cout << endl;

	for (auto& str : ret2)
	{
		cout << str << " ";
	}
	cout << endl;

	return 0;
}

在这里插入图片描述

👉queue 的介绍和使用👈

queue 的介绍

  1. 队列是一种容器适配器,专门用于在 FIFO 上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
  2. 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue 提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
  3. 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
    • empty:检测队列是否为空
    • size:返回队列中有效元素的个数
    • front:返回队头元素的引用
    • back:返回队尾元素的引用
    • push_back:在队列尾部入队列
    • pop_front:在队列头部出队列
  4. 标准容器类 deque 和 list 满足了这些要求。默认情况下,如果没有为 queue 实例化指定容器类,则使用标准容器 deque。

在这里插入图片描述

queue 的使用

函数声明接口说明
queue()构造空的队列
empty()检测队列是否为空,是返回 true,否则返回 false
size()返回队列中有效元素的个数
front()返回队头元素的引用
back()返回队尾元素的引用
push()在队尾将元素 val 入队列
pop()将队头元素出队列

👉stack 的模拟实现👈

学习 stack 的模拟实现前,我们需要了解一下上面是设计模式。设计模式是一套被反复使用、多数人知晓的经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。

目前,我们学习到了一种设计模式 — 迭代器模式。迭代器模式不暴露底层的实现细节,封装后提供统一的方式访问容器。而接下来将要学习到的适配器模式就是将已有的东西封装转换成我们想要的东西。

stack 的适配器可以是 vector、list 和 deque,这些容器都支持尾插、尾删、判空和获得尾部元素等操作。stl 中的 stack 和 queue 的默认适配器都是双端队列 deque,而本人设计的 stack 默认适配器为 vector。注:双端队列将会在下面的内容里讲解。

// Stack.h
#pragma once
#include <vector>
#include <list>
#include <deque>
#include <iostream>
using namespace std;

namespace Joy
{
	// 默认容器为vector
	template <class T, class Container = vector<T> >
	class stack
	{
	public:
		void push(const T& val)
		{
			_con.push_back(val);
		}

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

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

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

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

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

	private:
		Container _con;
	};
}

// Test.cpp
#include "Stack.h"

int main()
{
	Joy::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	st.push(5);

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

	return 0;
}

在这里插入图片描述

👉queue 的模拟实现👈

queue 的适配器需要支持头删、尾插、判空、获得头部元素和尾部元素等操作。因为 vector 没有pop_front头删接口且 vector 头删效率低,所以本人采用 list 作为 queue 的默认适配器。

// Queue.h
#pragma once
#include <deque>
#include <list>
#include <iostream>
using namespace std;

namespace Joy
{
	// 默认适配器为list
	template <class T, class Container = list<T> >
	class queue
	{
	public:
		void push(const T& val)
		{
			_con.push_back(val);
		}

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

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

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

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

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

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

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

	private:
		Container _con;
	};
}

// Test.cpp
#include "Queue.h"

int main()
{
	Joy::queue<int> q;

	q.push(1);
	q.push(2);
	q.push(3);
	q.push(4);
	q.push(5);

	while (!q.empty())
	{
		cout << q.front() << " ";
		q.pop();
	}
	cout << endl;

	return 0;
}

在这里插入图片描述

在这里插入图片描述

👉容器适配器👈

什么是适配器

适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。

在这里插入图片描述

STL标准库中 stack 和 queue 的底层结构

虽然 stack 和 queue 中也可以存放元素,但 STL 并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为 stack 和 queue 只是对其他容器的接口进行了包装,STL 中 stack 和 queue 默认使用 deque。

在这里插入图片描述
在这里插入图片描述

deque

1. deque 的原理介绍

deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为 O(1)。与 vector 比较,头插效率高,不需要搬移元素;与 list 比较,空间利用率比较高。

deque 并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际 deque 类似于一个动态的二维
数组
,其底层结构如下图所示:


在这里插入图片描述

那 deque 是如何借助其迭代器维护其假想连续的结构呢?

在这里插入图片描述

#include <deque>
#include <iostream>
using namespace std;

int main()
{
	deque<int> d;
	d.push_back(1);
	d.push_back(2);
	d.push_back(3);
	d.push_back(4);
	
	d.push_front(10);
	d.push_front(20);
	
	for (size_t i = 0; i < d.size(); ++i)
	{
		cout << d[i] << " ";
	}
	cout << endl;

	return 0;
}

在这里插入图片描述

2. deque 的缺陷

与 vector 比较,deque 的优势是:头部插入和删除时,不需要搬移元素,效率高.而且在扩容时,也不需要搬移大量的元素,因此其效率是比 vector 高的。

与 list 比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。

但是,deque 有一个致命缺陷:不适合遍历,因为在遍历时,deque 的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下。而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑 vector 和 list,deque 的应用并不多,而目前能看到的一个应用就是 STL 用其作为 stack 和 queue 的底层数据结构。deque 适用于中间插入删除少、头尾插入删除多、偶尔需要随机访问的场景。

性能对比

#include <deque>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;

// N个数据需要排序,vector+ 算法sort  deque+ sort
void Test()
{
	srand(time(0));
	const int N = 1000000;
	vector<int> v;
	v.reserve(N);

	deque<int> dq;
	for (int i = 0; i < N; ++i)
	{
		auto e = rand();
		v.push_back(e);
		dq.push_back(e);
	}

	int begin1 = clock();
	sort(v.begin(), v.end());
	int end1 = clock();

	int begin2 = clock();
	sort(dq.begin(), dq.end());
	int end2 = clock();

	printf("vector sort:%d\n", end1 - begin1);
	printf("dequeue sort:%d\n", end2 - begin2);
}

int main()
{
	Test();
	return 0;
}

在这里插入图片描述

原因:deque 的随机访问效率没有 vector 的随机访问效率高。

为什么选择 deque 作为 stack 和 queue 的底层默认容器

stack 是一种后进先出的特殊线性数据结构,因此只要具有 push_back() 和 pop_back() 操作的线性结构,都可
以作为 stack 的底层容器,比如 vector 和 list 都可以。queue 是先进先出的特殊线性数据结构,只要具有
push_back 和 pop_front 操作的线性结构,都可以作为queue 的底层容器,比如 list。但是 STL 中对 stack 和
queue 默认选择 deque 作为其底层容器,主要是因为结合了 deque 的优点,而完美的避开了其缺陷。

  1. stack 和 queue 不需要遍历(因此 stack 和 queue 没有迭代器),只需要在固定的一端或者两端进行操作。
  2. 在 stack 中元素增加时,deque 比 vector 的效率高(扩容时不需要搬移大量数据);queue 中的元素增加时,deque 不仅效率高,而且内存使用率高。

👉总结👈

本篇博客主要讲解了栈的几道经典例题:最小值、验证栈序列、逆波兰表达式求值和将中缀表达式转为后缀表达式、什么是适配器、以适配器模式实现 stack 和 queue 以及双端队列 deque 等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/143047.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

密集单目 SLAM 的概率体积融合

点击进入—>3D视觉工坊学习交流群笔者简述&#xff1a;这篇论文主要还是在于深度估计这块&#xff0c;深度估计由于硬件设备的不同是有很多方法的&#xff0c;双目&#xff0c;RGBD&#xff0c;激光雷达&#xff0c;单目&#xff0c;其中最难大概就是单目了。在该论文中作者…

Flutter不常用组件(四)

Offstage 创建一个在视觉上隐藏其子项的小部件。隐藏后不占空间。 该组件有以下几个属性&#xff1a; Key? key&#xff1a;标识键bool offstage true&#xff1a;是否隐藏。默认为trueWidget? child&#xff1a;子组件 Center(child: Column(mainAxisAlignment: MainAx…

【设计篇】36 # 如何理解可视化设计原则?

说明 【跟月影学可视化】学习笔记。 可视化设计的四个基本原则 基本原则一&#xff1a;简单清晰原则 我们可以看下面一张图&#xff1a;国际茶叶委员会制作的全球茶叶消费排行榜图表&#xff0c;目的是想说明喝茶最多的不是中国人 我们可以用更简单的直方图方式去表现 基本…

c++11 标准模板(STL)(std::deque)(九)

定义于头文件 <deque> std::deque 修改器 移除末元素 std::deque<T,Allocator>::pop_back void pop_back(); 移除容器的最末元素。 在空容器上调用 pop_back 是未定义的。 指向被擦除元素的迭代器和引用被非法化。尾后迭代器是否被非法化是未指定的。其他迭代…

YOLOv5-common.py文件

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊|接辅导、项目定制 目录一、任务说明二、导入相关包和配置1.基本组件1.1 autopad1.2 Conv1.3 Focus1.4 Bottleneck1.5 BottleneckCSP1.6 C31.7 SPP1.8 Concat1.…

C3P0数据库连接池详解 及连接JDBC步骤

目录 一、基本定义 二、使用C3P0&#xff08;数据库连接池&#xff09;的必要性 1.JDBC传统模式开发存在的主要问题 三、数据库连接池的详细说明 四、使用连接池的明显优势 1.资源的高效利用 2.更快的系统反应速度 3.减少了资源独占的风险 4.统一的连接管理&#xff0c…

SQL优化实战-0001:SQL查找是否存在,不要再使用count

文章目录1.需求分析与普遍SQL写法2.问题分析3.优化方案4.总结5.补充5.1 还有什么时候可以使用LIMIT 15.2 什么时候没必要使用LIMIT 11.需求分析与普遍SQL写法 业务代码中&#xff0c;需要根据一个或多个条件&#xff0c;查询是否存在记录而不关心有多少条记录。普遍的SQL及代码…

大战谷歌!微软Bing引入ChatGPT;羊了个羊高·薪招纳技术人才;Debian彻底移除Python2;GitHub今日热榜 | ShowMeAI资讯日报

&#x1f440;日报合辑 | &#x1f3a1;AI应用与工具大全 | &#x1f514;公众号资料下载 | &#x1f369;韩信子 &#x1f3a1; 『微软Bing』将引入 ChatGPT&#xff0c;与 Google 一场大战难免 微软计划2023年3月底之前推出 Bing 搜索引擎的新版本&#xff0c;使用 ChatGPT …

Lua中self 、自索引及其面向对象应用代码示例

一、Lua表的self标识 在lua中&#xff0c;表拥有一个标识&#xff1a;self。self类似于c中的this指针和python中的self。在lua中&#xff0c;提供了冒号操作符来隐藏这个参数&#xff0c;例如&#xff1a; t1 {id 1, name "panda",addr "beijing" }-…

快过年了,看我使用python制作一个灯笼的小程序

哈喽呀&#xff0c;最近一直在写一些关于新年的的python小东西&#xff0c;今天也不例外&#xff0c;我把那些都放到了一个专辑里面了&#xff0c;感兴趣的朋友们可以去看看一下哦 目录 前言 画外轮廓 画灯笼的提线 画灯笼盖 画一下各种柱子 小小的外壳来一下 其他的小装饰…

Python打包(问题记录,待解决)

引言 文章用于测试在Python3.8的版本&#xff0c;打包Obspy地震包&#xff0c;最后集成到PyQt5上。 部署或冻结应用程序是 Python 项目的重要组成部分&#xff0c; 这意味着捆绑所有必需的资源&#xff0c;以便应用程序找到它需要的一切 能够在客户端计算机上运行。 但是&…

基于Python Unet的医学影像分割系统源码,含皮肤病的数据及皮肤病分割的模型,用户输入图像,模型可以自动分割去皮肤病的区域

手把手教你用Unet做医学图像分割 我们用Unet来做医学图像分割。我们将会以皮肤病的数据作为示范&#xff0c;训练一个皮肤病分割的模型出来&#xff0c;用户输入图像&#xff0c;模型可以自动分割去皮肤病的区域和正常的区域。废话不多说&#xff0c;先上效果&#xff0c;左侧…

JAVA语言基础语法——JVM虚拟机默认处理异常的方式,try...catch捕获异常

1.JVM默认的处理方式 a&#xff0e;把异常的名称&#xff0c;异常原因及异常出现的位置等信息输出在了控制台 运行结果如下&#xff1a; b&#xff0e;程序停止执行&#xff0c;异常下面的代码不会再执行了 2.try...catch捕获异常&#xff08;自己处理&#xff0c;捕获异常&am…

数学建模-相关性分析(Matlab)

注意&#xff1a;代码文件仅供参考&#xff0c;一定不要直接用于自己的数模论文中 国赛对于论文的查重要求非常严格&#xff0c;代码雷同也算作抄袭 如何修改代码避免查重的方法&#xff1a;https://www.bilibili.com/video/av59423231 //清风数学建模 一、基础知识 1.皮尔逊…

Qt之Json操作demo

一、JSON简介&#xff1a; JSON(JavaScript Object Notation)是一种轻量级的数据交换格式&#xff0c;使用JavaScript语法来描述数据对象&#xff0c;但是JSON仍然独立于语言和平台。JSON解析器和JSON库支持许多不同的编程语言&#xff0c;被广泛用于Internet上的数据交换格式。…

多线程高级(线程状态、线程池、volatile、原子性、并发工具)

1.线程池 1.1 线程状态介绍 当线程被创建并启动以后&#xff0c;它既不是一启动就进入了执行状态&#xff0c;也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢&#xff1f;Java中的线程 状态被定义在了java.lang.Thread.State…

Java程序:jstack

前言 如果有一天&#xff0c;你的Java程序长时间停顿&#xff0c;也许是它病了&#xff0c;需要用jstack拍个片子分析分析&#xff0c;才能诊断具体什么病症&#xff0c;是死锁综合征&#xff0c;还是死循环等其他病症&#xff0c;本文我们一起来学习jstack命令~ jstack 的功能…

阳康后的第一篇博客,先来几道恶心二进制编程题

目录 一、统计二进制中1的个数 二、打印整数二进制的奇数位和偶数位 三、两个整数二进制位不同个数 一、统计二进制中1的个数 这是一道牛客网OJ题&#xff0c;感兴趣的话可以先做一遍再看解析哦 -> 牛客网的OJ链接 注意&#xff1a;上面的牛客网是接口型&#xff0c;不需…

Vagrant管理已存在的虚拟机

起因 某天打开VirtualBox后&#xff0c;发现之前创建的虚拟机都没了&#xff0c;后将虚拟机从本地磁盘又重新导入&#xff0c;但是发现使用 vagrant up 会创建新的虚拟机&#xff0c;而我要用vagrant管理已存在的虚拟机&#xff0c;就是 vagrant up的时候&#xff0c;我需要启动…

MySQL数据库:常用数据类型

一、整形和浮点型 数据类型大小说明对应C类型bit(n)n指定比特位数&#xff0c;默认1位比特位数&#xff0c;n范围1-64&#xff1b;存储数值范围2-2^n-1char[]tinyint1字节signed charsmallint2字节short intint4字节intbigint8字节long long intfloat(m,d)4字节单精度&#xf…