C++必修:STL之stack与queue的使用与实现

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C++学习
贝蒂的主页:Betty’s blog

1. stack与queue的介绍

1.1. stack

stack就是我们数据结构中所学习的栈,它遵循后进先出(Last In First Out,LIFO)的原则。具体来说就就是STL对与栈这个数据结构的封装,使用时包含头文件#include<stack>即可。

img

img

1.2. queue

queue就是我们数据结构中所学习的队列,遵循先进先出(First-In-First-Out,FIFO)的原则。具体来说就就是STL对与队列这个数据结构的封装,使用时包含头文件#include<queue>即可。

img

img

1.3. 容器适配器

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

这中模式就在我们STL中的stackqueue得到了适用。为了防止重复造轮子,stackqueue在实现时就复用了STL中的其他容器。我们打开相关文档就知道stackqueue有两个模版参数,一个是容器的类型,一个就是容器适配器。

img

img

其中需要注意的就是:

  • stack的容器适配器需要支持emptysizebackpush_backpop_back五种功能,其中常见的容量有vectordequelist
  • stack的容器适配器需要支持emptysizefrontbackpush_backpop_front五种功能,其中常见的容量有dequelist

那么为什么STL库中会选择deque作为stackqueue的默认容器呢?原因如下:

  1. deque 适用于 stack 的原因:
  • deque 头插头删、尾插尾删效率高,契合 stack 的操作需求。
  • 元素增长时,dequevector 效率高,因为扩容时不需要搬移大量数据。
  1. deque 适用于 queue 的原因:
  • 头插头删、尾插尾删效率高,符合 queue 的操作特性。
  • 元素增长时,不仅效率高,而且相比 list 内存使用率高,因为 list 每个节点有额外指针开销,内存不够紧凑。并且大量数据时,list容易造成内存碎片。

2. stack与queue的使用

2.1. stack的使用

stack提供的接口相比较与其他容量明显较少,这是由于其结构决定的,并且因为stack也并不需要遍历,所以也不存在迭代器的概念。

以下就是stack的接口:

img

因为我们学习过数据结构中的栈,所以使用这里的接口的成本也比较低

void Test1()
{
	stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	st.push(5);
	cout << "容量大小为:" << st.size() << endl;
	cout << "出栈顺序为:";
	while (!st.empty())
	{
		int top = st.top();
		st.pop();
		cout << top << " ";
	}
	cout << endl;
}

img

当然我们也可以选择其他容器作为容器适配器。

void Test2()
{
	//选用vector作为容器适配器
	stack<int,vector<int>> st1;
	st1.push(1);
	st1.push(2);
	st1.push(3);
	st1.push(4);
	st1.push(5);
	cout << "出栈顺序为:";
	while (!st1.empty())
	{
		int top = st1.top();
		st1.pop();
		cout << top << " ";
	}
	cout << endl;
	//选用list作为容器适配器
	stack<int, list<int>> st2;
	st2.push(1);
	st2.push(2);
	st2.push(3);
	st2.push(4);
	st2.push(5);
	cout << "出栈顺序为:";
	while (!st2.empty())
	{
		int top = st2.top();
		st2.pop();
		cout << top << " ";
	}
}

img

2.2. queue的使用

queue提供的接口相比较与其他容量明显较少,这是由于其结构决定的,并且因为queue也并不需要遍历,所以也不存在迭代器的概念。

以下就是queue的接口:

img

因为我们学习过数据结构中的队列,所以使用这里的接口的成本也比较低

void Test3()
{
	queue<int> q;
	q.push(1);
	q.push(2);
	q.push(3);
	q.push(4);
	q.push(5);
	cout << "容量大小为:" << q.size() << endl;
	cout << "队首:" << q.front() << endl;
	cout << "队尾:" << q.back() << endl;
	while (!q.empty())
	{
		int val = q.front();
		q.pop();
		cout << val << " ";
	}
	cout << endl;
}

img

当然我们也可以选择其他容器作为容器适配器。

void Test4()
{
	queue<int,list<int>> q;
	q.push(1);
	q.push(2);
	q.push(3);
	q.push(4);
	q.push(5);
	while (!q.empty())
	{
		int val = q.front();
		q.pop();
		cout << val << " ";
	}
	cout << endl;
}

img

3. stack与queue的模拟实现

3.1. 模拟实现stack

stack借助容器适配器的话实现就非常简单,我们只需要复用接口即可,一般我们将容器的末尾作为栈顶。

#include <deque>

namespace betty
{
	//默认使用deque适配器
	template <class T, class Container = deque<T>>
	class stack
	{
	public:
		//判断是否为空
		bool empty()
		{
			return _con.empty();
		}
		size_t size()
		{
			return _con.size();
		}
		//取栈顶数据
		const T& top()
		{
			return _con.back();
		}
		//压栈
		void push(const T& x)
		{
			_con.push_back(x);
		}
		//出栈
		void pop()
		{
			_con.pop_back();
		}
	private:
		Container _con;
	};
}

3.2. 模拟实现queue

queue借助容器适配器的话实现也非常简单,我们只需要复用接口即可,一般我们将容器的末尾作为队尾,容器的起始作为队头。

namespace betty
{
	//默认使用deque适配器
	template <class T, class Container = deque<T>>
	class queue
	{
	public:
		//判断是否为空
		bool empty()
		{
			return _con.empty();
		}
		size_t size()
		{
			return _con.size();
		}
		//取队头数据
		const T& front()
		{
			return _con.front();
		}
		//取队尾数据
		const T& back()
		{
			return _con.back();
		}
		//队尾入数据
		void push(const T& x)
		{
			_con.push_back(x);
		}
		//队头出数据
		void pop()
		{
			_con.pop_front();
		}
	private:
		Container _con;
	};
}

4. 逆波兰表达式

逆波兰表达式简介逆波兰表达式,也叫后缀表达式,是一种将运算符放在操作数后面的表达式表示方法。 在常规的中缀表达式中,运算符位于操作数之间,例如 2 + 3。而在逆波兰表达式中,会表示为 2 3 +。 其优点在于计算时无需考虑运算符的优先级,直接从左到右依次计算即可。例如计算 3 4 + 5 *,先计算 3 + 4得到 7,再计算 7 * 5得到最终结果 35

对于计算机来说常规的中缀表达式是无法直接计算的,因为涉及优先级问题。所以为了让计算机能够直接运算,我们首先要把中缀表达式转为逆波兰表达式。

4.1. 中缀转后缀

中缀表达式转后缀表达式的方法如下:

  1. 初始化一个栈,用于保存暂时不能确定运算顺序的运算符。
  2. 从左到右处理各个元素,直到末尾,可能遇到以下三种情况:
  • 遇到操作数,直接输出(加入后缀表达式)。

  • 遇到界限符即()

    • 遇到(,不参与优先级比较直接入栈。
    • 遇到),要参与比较,认为)的优先级最低,依次pop掉栈顶的运算符输出(加入后缀表达式),直至遇到(停止,(直接 pop 掉不输出。
  • 遇到运算符/操作符:

    • 如果是-先判断是操作符还是负数,如果是负数输出0与入栈-相当于将负数转化为正数。
    • 如果栈为空,直接入栈,认为()优先级最低。
    • 如果栈不为空,跟栈顶操作符进行优先级比较,栈顶是(也比较:
      • 比栈顶操作符优先级高,入栈,然后处理下一个元素。
      • 比栈顶操作符优先级低或相等,pop 掉栈顶操作符并输出(加入后缀表达式)。
  1. 最后遍历完所有元素,再去判断栈是否为空。不为空则依次pop掉栈顶的所有运算符输出(加入后缀表达式)。

例如,对于中缀表达式2 + 3 * ( 4 - 1 ),按照上述规则进行转换。首先处理数字 2 和 3,直接输出。遇到+,栈为空,入栈。遇到*,此时栈顶为+*优先级高,入栈。遇到(,入栈。遇到 4 和 1 直接输出。遇到-,栈顶为(,入栈。处理完4 - 1后,遇到),开始pop栈顶运算符输出,直到遇到(停止。最终得到后缀表达式2 3 4 1 - * +

// 比较运算符的优先级,返回 1 表示 '+' 或 '-',返回 2 表示 '*' 或 '/'
int precedence(char op) {
	if (op == '+' || op == '-')
		return 1;
	if (op == '*' || op == '/')
		return 2;
	return 0;
}

// 将中缀表达式转换为后缀表达式的函数,并返回一个 vector<string>
vector<string> infixToPostfix(string& infix) 
{
	stack<char> opStack;  // 用于存储运算符的栈
	vector<string> postfix;  // 存储转换后的后缀表达式
	int i = 0;
	bool isPrevOperand = false;  // 标记前一个元素是否为操作数
	//false为负数,true为操作符-
	while (i < infix.size()) 
	{
		//负数
		if (infix[i] == '-'&&isPrevOperand==false)
		{
			i++;
			postfix.push_back("0");
			opStack.push('-');
			isPrevOperand = true;//0作为操作数
		}
		char c = infix[i];
		if (isspace(c)) {  // 如果是空格则跳过
			i++;
			continue;
		}
		
		if (isdigit(c))
		{  
			string num;
			//判断是否是多位数
			while (i < infix.size() && isdigit(infix[i])) {
				num += infix[i];
				i++;
			}
			postfix.push_back(num);
			isPrevOperand = true;//这个多位数作为操作数
		}
		else if (c == '(') {  // 如果是 '('
			opStack.push(c);
			i++;
			isPrevOperand = false;//(相当于表达式开始
		}
		else if (c == ')') {  // 如果是 ')'
			while (!opStack.empty() && opStack.top() != '(') {
				string op;
				op += opStack.top();
				postfix.push_back(op);
				opStack.pop();
			}
			if (!opStack.empty() && opStack.top() == '(') {
				opStack.pop();
			}
			i++;
			isPrevOperand = true;//()的结果当做操作数
		}
		else {  // 如果是运算符
			while (!opStack.empty() && precedence(opStack.top()) >= precedence(c)) {
				string op;
				op += opStack.top();
				postfix.push_back(op);
				opStack.pop();
			}
			opStack.push(c);
			i++;
		}
	}
    //剩余操作符依次弹出
	while (!opStack.empty()) {
		string op;
		op += opStack.top();
		postfix.push_back(op);
		opStack.pop();
	}

	return postfix;  // 返回转换后的后缀表达式
}

问题一:为什么将负数转换为0-正数 ?

在处理中缀表达式时,负数的情况可能会带来一些麻烦。例如,单纯的-3这样的表达式,如果按照常规方式理解,会有一个操作符和一个操作数。然而,这样在利用后缀表达式进行计算时会变得难以处理。 为了更有效地处理这种情况,我们采取一种优化策略,即将其转化为0 - 正数的形式。通过这样的转换,就能保证每个操作符都对应两个操作数,使得后续的计算更加清晰和准确。 比如,对于表达式-3,我们将其转换为0 - 3。这种处理方式有助于在使用后缀表达式进行计算时,避免因负数引起的逻辑复杂性和错误。

问题二:为什么比栈顶运算符优先级高就入栈,反之则弹出栈顶运算符?

当遇到一个新的运算符时,如果其优先级比栈顶运算符的优先级低,这就意味着前一个表达式应该先进行运算。而如果新运算符的优先级比栈顶运算符的优先级高,由于无法确定后续是否还会有更高优先级的运算符出现,所以不能确定当前新运算符对应的表达式何时运算,此时应将其继续入栈。 例如,栈顶是+,新运算符是*,因为*的优先级高,所以先将*入栈;但如果新运算符是-,其优先级低于*,则先运算栈顶的*相关的表达式。

4.2. 计算后缀表达式

在学习完如何将中缀表达式转化为后缀表达式之后,接下来让我们来学习如何运算后缀表达式。

后缀表达式的运算过程如下:

  1. 首先,创建一个栈 st 用于存储操作数。
  2. 遍历后缀表达式的每个元素(存储在 tokens 向量中)。
  3. 如果元素是运算符(+-*/):
  • 从栈中弹出两个操作数,分别记为 rightleft
  • 根据运算符进行相应的计算(加法、减法、乘法、除法)。
  • 将计算结果压入栈中。
  1. 如果元素是操作数(数字),将其转换为整数并压入栈中。

  2. 遍历结束后,栈顶元素即为后缀表达式的计算结果,返回该值。

例如,对于后缀表达式 ["2", "1", "+", "3", "*"] ,首先将 21 压入栈,遇到 + 时弹出 12 计算 2 + 1 = 3 并压入栈,再遇到 3 压入栈,遇到 * 时弹出 33 计算 3 * 3 = 9 ,最终栈顶元素 9 即为结果。

// 计算逆波兰表达式(后缀表达式)的函数
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();  // 返回栈顶元素,即计算结果
}
h(left - right);  // 执行减法并将结果入栈
                    break;
                case '*':
                    st.push(left * right);  // 执行乘法并将结果入栈
                    break;
                case '/':
                    st.push(left / right);  // 执行除法并将结果入栈
                    break;
            }
        }
        else {  // 如果是操作数
            st.push(stoi(str));  // 将操作数转换为整数并入栈
        }
    }

    return st.top();  // 返回栈顶元素,即计算结果
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Betty’s Sweet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值