目录
栈的概述
栈(Stack)是一种基本的数据结构,它按照特定的顺序进行数据的插入和删除操作,这种顺序通常被称作“后进先出”(Last In First Out, LIFO)。想象一下一摞盘子,你总是先放最后一个盘子在最上面,取的时候也总是从最上面的盘子开始取,这就是栈的原理。
本文将使用C++对栈进行封装。
栈有什么用?
我们以FPS游戏逃离塔科夫/暗区突围举例,假设你在编写游戏的射击系统,在游戏中一把枪可以压入不同类型或等级的子弹,假如一把枪30发子弹,你先装了10发AP弹,然后装了10发达姆弹,然后又装了9发AP弹,最后一发装了达姆弹(虽然正常来说没人这么装)。那么作为开发者,我们就要保证这把枪中子弹的出枪顺序为:
1发达姆弹 ——> 9发AP弹 ——> 10发达姆弹 ——> 10发AP弹
这时,如果我们用栈来编写弹夹的代码,那么最先装如弹夹(入栈)的10发AP弹就会最后射出(出栈),最后装入弹夹1发达姆弹就会最先射出。
知识基础
阅读本文,你需要具备以下知识基础:
- C++基础知识:类的封装、模版类、操作符重载。
- 代码能力:单链表的实现。
栈的基本操作
在栈中,允许进行数据插入和删除的一端称为栈顶,另一端称为栈底。栈的基本操作主要有两个:
- 压栈(Push):在栈顶插入一个元素。这个操作将新元素放到栈顶,成为新的栈顶元素。
- 出栈(Pop):从栈顶移除一个元素。这个操作将移除当前的栈顶元素,使其下一个元素成为新的栈顶。
除了这两个基本操作外,栈通常还有以下几个附加操作:
- 查看栈顶元素(Retrieve/Top):查看栈顶的元素而不移除它。
- 判断栈是否为空(Empty):检查栈中是否含有元素。
- 获取栈的大小(Size):返回栈中元素的个数。
- 清空栈(Clear):将栈内元素清空。
栈的存储方式
在编程语言中,栈可以通过数组或者链表来实现。使用数组实现的栈称为顺序栈,使用链表实现的栈称为链式栈。两种实现方式各有优缺点:顺序栈访问元素更快,实现更加简单,但是大小固定,可能存在栈满的情况;链式栈的大小动态可变,但是访问元素需要通过指针,速度相对较慢。
在实际应用中,我们更多的将是使用链表栈,因此,本文只进行链表栈实现的讲解。
代码实现(C++)
类头(LinkedStack.h)
#ifndef LINKEDSTACK_H
#define LINKEDSTACK_H
#define NULL nullptr
template<typename T> struct StackNode //链表节点
{
T data;
StackNode<T> *next;
};
template<typename T> class LinkedStack
{
private:
StackNode<T> *head; //指向栈顶
public:
LinkedStack<T> &operator=(const LinkedStack<T> &other);
LinkedStack(); //构造函数
LinkedStack(const LinkedStack<T> &other); //拷贝构造函数
void push(T data); //压栈
void top(T &data) const; //获取顶部元素
void pop(); //出栈
bool empty() const; //判断栈是否为空
void clear(); //清空栈
int size() const; //返回栈内元素个数
LinkedStack<T>& operator=(LinkedStack<T>& other); //操作符重载=
~LinkedStack(); //析构函数
};
#endif //LINKEDSTACK_H
类的方法实现(LinkedStack.cpp)
构造函数
template<typename T>
LinkedStack<T>::LinkedStack() {
this->head=NULL;
}
拷贝构造函数
template<typename T>
LinkedStack<T>::LinkedStack(const LinkedStack<T> &other) {
for (StackNode<T> *p=other.head; p!=NULL; p=p->next) {
this->push(p->data); //遍历other进行复制
}
}
析构函数
template<typename T>
LinkedStack<T>::~LinkedStack() {
clear();
}
压栈(push)
template<typename T>
void LinkedStack<T>::push(T data) {
StackNode<T> *p=new StackNode<T>; //实例化(创建)一个新的节点
p->data=data;
p->next=this->head; //将新节点的next指针指向当前栈顶
this->head=p; //将新节点设为新的栈顶
}
获取栈顶元素(top)
template<typename T>
void LinkedStack<T>::top(T &data) const{
if (!empty()) { //先判断栈是否为空
data=this->head->data;
}
}
出栈(pop)
template<typename T>
void LinkedStack<T>::pop() {
if (!empty()) { //先判断栈是否为空
StackNode<T> *p=this->head; //使用临时指针记录栈顶地址
/*状态1*/
this->head=p->next; //将新栈顶设置为原栈顶指向的元素
/*状态2*/
delete p; //删除原栈顶
/*状态3*/
return;
}
}
判断栈是否为空(empty)
template<typename T>
bool LinkedStack<T>::empty() const {
if (this->head==NULL) return true;
else return false;
}
清空栈(empty)
template<typename T>
void LinkedStack<T>::clear() {
while (!empty()) { //将栈中所有元素全部pop出栈
pop();
}
}
获取栈的大小(size)
template<typename T>
int LinkedStack<T>::size() const {
int i=0;
StackNode<T> *p=this->head;
while (p!=NULL) { //遍历计数
i++;
p=p->next;
}
return i;
}
操作符重载=
template<typename T>
LinkedStack<T> & LinkedStack<T>::operator=(LinkedStack<T> &other) {
LinkedStack<T> temp(other);
this->head=temp.head;
/*状态1*/
temp.head=NULL; //防止复制的栈被析构
/*状态2*/
return *this;
/*状态3*/
}
如果没有temp.head=NULL
这句,那么那么temp
和this
将指向同一个栈(状态1),那么在函数return后,这个栈将会被temp
的析构函数析构掉,因为temp
的生命周期已经结束。
*此处也可以使用像拷贝构造函数中一样直接遍历复制的方法
测试
这里给出一个main
函数用来测试栈的相关功能。
#include <iostream>
#include "LinkedStack.cpp" //使用模版类时直接使用.cpp文件,而不是.h文件
using namespace std;
int main() {
cout<<"Type in an integer n followed by n decimal numbers."<<endl
<<"The numbers will be printed in reverse order."<<endl;
int n;
cin>>n;
LinkedStack<int> all;
while(n--) {
int i;
cin>>i;
all.push(i); //测试push()
}
LinkedStack<int> temp(all); //测试拷贝构造函数
LinkedStack<int> temp1; //测试操作符重载
temp1=temp;
while(!temp1.empty()) { //测试empty()
int i;
temp1.top(i); //测试top()
cout<<i<<" ";
temp1.pop(); //测试pop()
}
cout<<endl;
return 0;
}
练习:逆波兰计算器
后缀表达式是为了解决计算机在进行多项式运算时不能加括号的问题,它将运算符号置于运算数字之后。如:
( 5 + 2 ) - 8 / 2 = 3
转化为后缀表达式后,将变为:
5 2 + 8 2 / - = 3
请编写程序完成逆波兰计算器,以实现对后缀表达式的运算,程序运行效果如下:
答案:
#include<iostream>
#include"LinkedStack.cpp"
using namespace std;
class P_cal {
LinkedStack<double> all;
void in_stack() {
cout<<"Enter a real number:";
double i;
cin>>i;
this->all.push(i);
}
void calculate(char c) {
if (this->all.size()==1) {
cout<<"Stack has just one entry"<<endl;
return;
}
double a,b;
this->all.top(b);
this->all.pop();
this->all.top(a);
this->all.pop();
switch (c)
{
case '+':
this->all.push(a+b);
break;
case '-':
this->all.push(a-b);
break;
case '*':
this->all.push(a*b);
break;
case '/':
this->all.push(a/b);
break;
//case '^':
// this->all.push(pow(a,b));
default:
break;
}
}
void output_top() {
double temp;
this->all.top(temp);
cout<<temp<<endl;
}
public:
void Main_manu() {
cout<<"This is a reverse Polish Calculator."<<endl;
cout<<"Please enter a valid command:"<<endl
<<"[?]push to stack [=]print top"<<endl
<<"[+] [-] [*] [/] are arithmetic operations"<<endl
<<"[Q]uit."<<endl;
while (1) {
cout<<"Select command and press <Enter>:";
char order;
cin>>order;
if (order=='?') {this->in_stack();}
else if (order=='+'||order=='-'||order=='*'||order=='/') {this->calculate(order);}
else if (order=='=') {this->output_top();}
else if (order=='q') {break;}
else {
cout<<"Please enter a valid command:"<<endl
<<"[?]push to stack [=]print top"<<endl
<<"[+] [-] [*] [/] are arithmetic operations"<<endl
<<"[Q]uit."<<endl;
continue;
}
}
}
};
int main() {
P_cal a;
a.Main_manu();
cout<<"Calculate finished."<<endl;
return 0;
}