C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++ stack和queue底层实现

深入探索C++中stack和queue的底层实现

作者:春人.

这篇文章主要介绍了C++中的stack和dequeue的底层实现,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

一、stack的介绍和使用

1.1 stack的介绍

在这里插入图片描述

1.2 stack的使用

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

1.2.1 最小栈

在这里插入图片描述

本题的思路是用两个栈来实现,其中一个栈 _st 用来正常存储数据,另一个栈 _minst 用来存储最小的数据。具体实现就是在往 _st 中插入数据的时候进行判断,如果当前插入的数据 val 小于等于 _minst 栈顶的数据,那就将 val 也插入到 _minst 这个栈中。否则直将数据插入 _st 中。在 pop 数据的时候,先取 _st 的栈顶元素和 _minst 的栈顶元素进行比较,如果二者相等,那就同时 pop _st _minst 的栈顶元素,否则就值 pop _st 的栈顶元素。要获取堆栈中的最小元素直接返回 _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();
    }
private:
    stack<int> _st;
    stack<int> _minst;
};

1.2.2 栈的压入、弹出序列

在这里插入图片描述

本题的解题思路是用一个栈来模拟。即先定义一个栈 st 然后给栈中入一个数据,接着取栈顶的数据和出栈序列 popV 当前位置元素进行比较进行比较,如果不相等则继续从入栈序列 pushV 中拿数据往栈 st 中入,如果相等就出栈。这里需要注意,有可能需要连续多次出栈。直到最终将入栈序列 pushV 中的数据全入栈,最后判断栈 st 是否为空,如果为空,就说明该出栈序列正确。如果不空就说明该出栈序列有问题。

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pushV int整型vector 
     * @param popV int整型vector 
     * @return bool布尔型
     */
    bool IsPopOrder(vector<int>& pushV, vector<int>& popV) 
    {
        // write code here
        stack<int> st;
        size_t push_pos = 0, pop_pos = 0;
        while(push_pos < pushV.size())
        {
            //先入一个元素
            st.push(pushV[push_pos++]);
            if(st.top() != popV[pop_pos])
            {
                //不匹配继续入数据
                continue;
            }
            while(!st.empty() && st.top() == popV[pop_pos])
            {
                //匹配,出数据
                st.pop();
                pop_pos++;
            }
        }
        return st.empty();
    }
};

代码优化:我们可以发现上面代码中不匹配逻辑里面其实啥也没干,因此我们可以把这段代码给删掉,上面加上是为了使逻辑更加清晰。

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pushV int整型vector 
     * @param popV int整型vector 
     * @return bool布尔型
     */
    bool IsPopOrder(vector<int>& pushV, vector<int>& popV) 
    {
        // write code here
        stack<int> st;
        size_t push_pos = 0, pop_pos = 0;
        while(push_pos < pushV.size())
        {
            //先入一个元素
            st.push(pushV[push_pos++]);
            while(!st.empty() && st.top() == popV[pop_pos])
            {
                //匹配,出数据
                st.pop();
                pop_pos++;
            }
        }
        return st.empty();
    }
};

1.2.3 逆波兰表达式求值

在这里插入图片描述

逆波兰表达式也被叫做后缀表达式。什么是后缀表达式呢?先来了解一下中缀表达式,中缀表达式就是我们平时最常见的,例如: 2 + 3 ∗ 1 2+3*1 2+3∗1,就是一个典型的中缀表达式。将前面的中缀表达式变成后缀表达式得到: 2 1 3 * + ,这就是一个后缀表达式,后缀表达式相较于中中缀表达式,操作数顺序不变,操作符按优先级重排。这道题目就是要求我们对后缀表达式进行求解。求解后缀表达式,我们可以借助一个栈,遇到操作数入栈,遇到操作符从栈中出两个元素进行运算,将运算结果继续入栈。最终栈顶的元素就是整个逆波兰表达式的结果。

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        size_t pos = 0;
        while(pos < tokens.size())
        {
            if(tokens[pos] != "+" && tokens[pos] != "-" && tokens[pos] != "*" && tokens[pos] != "/")
            {
                //如果是数字就入栈
                int num = stoi(tokens[pos]);
                st.push(num);
            }
            else
            {
                //不是数字就从栈中取两个元素出来
                int val1 = st.top();
                st.pop();
                int val2 = st.top();
                st.pop();
                int ret = 0;
                if(tokens[pos] == "+")
                {
                    ret = val2 + val1;
                }
                else if(tokens[pos] == "-")
                {
                    ret = val2 - val1;
                }
                else if(tokens[pos] == "*")
                {
                    ret = val2 * val1;
                }
                else if(tokens[pos] == "/")
                {
                    ret = val2 / val1;
                }
                //将计算结果继续入栈
                st.push(ret);
            }
            pos++;
        }
        return st.top();
    }
};

注意:在写上面这段代码的时候有下面几点需要特别注意,首先这是一个 string 数组,会涉及到 string int 。其次需要注意在从栈中取数的时候,第一次取出的是右操作数,第二次取出的是左操作数,因此 val2 应该做左操作数, val1 应该做右操作数,尤其是减法运算和除法运算,这两个操作数的顺序必须得到保证。

补充:这里补充一个小知识点:如何将中缀表达式转换成后缀表达式。主要过程分为以下几步:

小Tips:当前操作符能否计算取决于后一个操作符的优先级是否高于自己,所以每当我们遇到一个操作符的时候,先不着急将它入栈,先和栈顶的操作符进行优先级比较,如果当前操作符的优先级比栈顶操作符的优先级低或者相等,我们就可以取出栈顶这个操作符进行运算。如果遇到括号可以走一个递归。其次就是需要想办法确定符号的优先级。

1.2.4 用栈实现队列

在这里插入图片描述

将一个栈当做输入栈,用于压入 push 传入的数据,另一个栈当做输出栈,用于 pop 和 peek 操作。每次 pop 或 peek 时,若输出栈为空则将输入栈的全部数据依次弹并压入输出栈,这样输出栈从栈顶往栈底的顺序就是队列从队首往队尾的顺序。

class MyQueue {
public:
    MyQueue() 
    {}
    void push(int x) 
    {
        input_st.push(x);
    }
    int pop() 
    {
        if(output_st.empty())
        {
            while(!input_st.empty())
            {
                output_st.push(input_st.top());
                input_st.pop();
            } 
        }
        int ret = output_st.top();
        output_st.pop();
        return ret;
    }
    int peek() 
    {
        if(output_st.empty())
        {
            while(!input_st.empty())
            {
                output_st.push(input_st.top());
                input_st.pop();
            }
        }
        return output_st.top();
    }
    bool empty() 
    {
        return input_st.empty() && output_st.empty();
    }
private:
    stack<int> input_st;
    stack<int> output_st;
};

二、queue的介绍和使用

2.1 queue的介绍

在这里插入图片描述

2.2 queue的使用

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

2.2.1 二叉树的层序遍历

在这里插入图片描述

二叉树的层序遍历可以借助队列来实现,从根节点开始,先让父节点入队列,在出队尾节点的同时,将该节点的左孩子和右孩子依次入队列。直到队列为空,从队列中出出来的结果就是层序遍历的结果。这道题目需要将同一层的所有节点都存入一个一维数组,再将这些一维数组组合成一个二维数组返回。我们前面的这种做法会导致队列中出现两层节点混在意的情况,因此我们可以定义一个变量 levelSize 来记录每一层的节点数。具体代码如下:

class Solution 
{
public:
    vector<vector<int>> levelOrder(TreeNode* root) 
    {
        vector<vector<int>> retV;
        queue<TreeNode*> qu;
        int levelSize = 0;
        qu.push(root);
        while(!qu.empty())
        {
            vector<int> tmp;
            levelSize = qu.size();
            while(levelSize != 0)
            {
                TreeNode* top = qu.front();
                if(top != nullptr)
                {
                    tmp.push_back(top->val);
                    qu.push(top->left);
                    qu.push(top->right);
                } 
                qu.pop();
                levelSize--;
            }
            if(!tmp.empty())
            {
                retV.push_back(tmp);
            }
        }
        return retV;
    }
};

三、模拟实现

3.1 stack模拟实现

template<class T, class Continer = vector<T>>
	class stack
	{
	public:
		stack()
		{}
		void push(const T& val)
		{
			_con.push_back(val);
		}
		void pop()
		{
			_con.pop_back();
		}
		T& top()
		{
			return _con.back();
		}
		size_t size()
		{
			return _con.size();
		}
		bool empty()
		{
			return _con.empty();
		}
	private:
		Continer _con;
	};

小Tips:stack 可以使用 vector 或者 list 来实现,效率相当。插入数据就相当于尾插,删除栈顶元素就相当于尾删。

3.2 queue模拟实现

template<class T, class Continer = std::list<T>>
class queue
{
public:
	queue()
	{}
	void push(const T& val)
	{
		_con.push_back(val);
	}
	void pop()
	{
		_con.pop_front();//这里不再支持vector
	}
	T& front()
	{
		return _con.front();
	}
	T& back()
	{
		return _con.back();
	}
	size_t size()
	{
		return _con.size();
	}
	bool empty()
	{
		return _con.empty();
	}
private:
	Continer _con;
};

小Tips:栈不能借助 vector 来实现,因为出队列,相当于删除 vector 中的第一个元素,而对 vector 头删会涉及挪动数据,效率相较于 list 会有所下降。

四、容器适配器

4.1 什么是适配器?

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

在这里插入图片描述

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

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

4.3 deque的简单介绍

4.3.1 deque的原理介绍

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

在这里插入图片描述

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

在这里插入图片描述

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了 deque 的迭代器身上,因此 deque 的迭代器设计的就比较复杂,如下图所示:

在这里插入图片描述

在这里插入图片描述

4.3.2 deque的缺陷

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

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

五、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下!

以上就是深入探索C++中stack和queue的底层实现的详细内容,更多关于C++ stack和queue底层实现的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文