在校学生,叙述中存在不足或错误之处还请各位批评斧正。
栈与线性表
前面我们学习过线性表的相关知识。在学习栈(stack)之前,我们观察一段代码:
#include<iostream>
using namespace std;
class Value {
protected: int data;
public: Value(const int v) { data = v; }
public: Value() { data = 0; }
public: ~Value() {
cout << "Object " << data <<" has been destroyed. "<<endl;
}
};
int main() {
Value v_1 = Value(1); //创建一个数值为1的对象
Value v_2 = Value(2);
Value v_3 = Value(3);
cout << "Finished..." << endl;
return 0;
}
对上面的程序运行,我们会有下面的结果:
我们可以看到当某一变量越往后创建时,反而会被最先释放释放
。加入我们将这里占用的内存比作一个线性表时,这里的空间管理遵循先进后出原则(First In Last Out, FILO)
。由于这个线性表的操作被限制,所以遵循这个限制性操作的限制性表叫做栈
,先前提到的那块内存我们叫做栈内存。
栈的定义
栈是一种操作被限制的线性表,在栈中空间管理遵循先进后出
原则,即当需要取出先进的数据元素,需要将后进的数据元素先取出。
栈的创建
明白了栈是怎么一回事,我们就需要构建一个栈。因为栈是一个线性表,所以我们继承
与顺序表或者是单向链表。
template<class T>
class Stack: public SeqList<T>; //继承于顺序表
template<class T>
class Stack: publc LinkList<T>; //继承于链表
在这里我们从头开始写一个栈,我们采用链表
的方式来管理栈中的数据元素。
栈的结点定义
结点的定义我们直接使用链表的结点,操作一致的。
template<class T> class Node {
protected:
T data;
Node<T>* nextnode;
public:
Node(); //创建一个空结点
Node(const T& d); //创建一个结点
Node(const T& d, Node<T>* const add);
~Node(); //销毁一个结点
public:
void setNextNode(Node<T>* const add); //设置一个结点的后继
Node<T>* getNextNode()const; //读取一个结点的后继
T getData(); //获取数据
};
栈的定义与实现
一个栈中我们需要明白需要什么方法,首先是最基础的添加元素
与删除元素
。在栈的数据结构中,向栈添加元素叫做推入push()
,删除元素叫做弹出pop()
。由于栈的结构遵循FILO原则,所以读取top()
元素只能栈顶元素(即最后一个进栈的元素)。
当然为了方便操作与管理,我们也添加一个判空方法empty()
返回boolean
值,一个返回大小的方法size()
。像电脑的栈内存中,由于空间有限,我们也可以限制最多存入几个数据,当到达上限时不再让元素入栈。这时可以添加一个full()
方法,确认栈是否满。
template<class T> class Stack {
protected:
Node<T>* head; //链表(栈)头
int SIZE; //栈的大小,这里表示有几个数据
public:
Stack(); //创建一个空栈
~Stack(); //销毁一个栈
public:
virtual void push(const T& obj); //将数据推入栈
virtual bool pop(); //将数据弹出栈
virtual T top(); //读取栈顶数据
public:
virtual bool empty()const; //判空方法
virtual int size()const; //返回数据个数
};
构造一个栈
与单链表一样,我们直接构造一个空栈
template<class T>
Stack<T>::Stack() {
head = new Node<T>(); //创建头结点,头结点后继默认指向nullptr
SIZE = 0;
}
将数据推入栈中
由于使用单链表,添加元素可使用头插法或者是尾插法,两个方法在栈中谁更好呢?
我们观察两个方法可以发现,当使用头插法时,链表中最后一个进入的元素必为head
的后继。头插法非常适合栈的内存管理,简化了方法的实现,方法时间复杂度为常数阶。
template<class T>
void Stack<T>::push(const T& obj) {
Node<T>* node = new Node(obj); //创建一个新结点
if(node == nullptr) { return; } //创建失败返回
node->setNextNode(head->getNextNode()); //传入head后继给新结点
head->setNextNode(node);
size++;
}
读取栈顶的数据
由于采用头插法向链表(栈)添加元素,所以当栈不为空时,栈顶即为head
的后继。
template<class T>
T Stack<T>::top() {
if(SIZE == 0) { return head->geatData(); } //空栈返回头结点
return head->getNextNode()->getData();
}
从栈中弹出元素
由于采用头插法向链表(栈)添加元素,所以当栈不为空时,栈顶即为head
的后继,我们弹出就是将head->getNextNode()
移除。
template<class T>
bool Stack<T>::pop() {
if(SIZE == 0) { return false; }
Node<T>* node = head->getNextNode(); //记录删除结点
head->setNextNode(head->getNextNode()->getNextNode()); //将head后继指向删除结点的后继
delete node;
SIZE--;
}
这个栈是否为空
判空我们有很多的方法,第一个时检查SIZE == 0
是否成立,第二是head
的有无后继,即head->getNextNode() == nullptr
是否成立。
template<class T>
bool Stack<T>::empty()const {
if(head == nullptr) { return true; }
return false;
}
返回栈内元素个数
template<class T>
bool Stack<T>::size()const { return SIZE; }
栈的应用——括号匹配
我们使用IDE(如:Visual Studio)时候,我们打出左括号时候,它会自行匹配右括号。如果我们不小心删除掉某个成对括号的一半时候,我们会得到一个ERROR
。现在我们也使用栈来实现这一小功能。
原理:
我们创建一个Judge
类,该类公共继承与Stack
类,里面的新增方法bool judgeKuohao(string str);
,该类接收一串仅含括号的字符串。然后返回匹配程度,如果均匹配返回true
,否则返回false
。
在bool judgeKuohao(string str)
具体实现上,采用从左遍历str
字符串,遇到左括号时往一个栈s
中压入,遇到右括号时从栈中弹出一个左括号。当完成遍历时如果执行s.empty()
理应返回true
。
方法实现
bool judgeBrackets(string str) {
while (empty() != true) { //清空栈
this->pop();
}
int str_l = str.length(); //获取字符串的长度
for (int i = 0; i < str_l; i++) { //遍历字符串(为方便,仅用小括号举例)
switch (str[i])
{
case '(':
this->push(str[i]); //推入左括号
break;
case ')':
if (this->pop() == false) {
return false;
}
break;
default:
break;
}
}
if (this->empty() == false) {
return false;
}
else {
return true;
}
}
栈的应用——后缀表达式
有上面的基础,我们研究第二个应用场景,允许用户输入一个四则运算的字符串后对算式进行运算。为简化算法的表示,我们对方法问题简化:①用户仅会输入0到9以内的数字;②有且仅有以下的算数运算符:加( + )、减( - )、乘( * )、除( \ )与开平方( sqrt() )。
引入新的知识点——后缀表达式:像平常我们常常书写的5 + 6 × (7 - 1)的式子叫做中缀表达式,中缀表达式适用于平常人们理解,而前面的计算表达式,也有另一种表达方式——“5 6 + 7 1 - ×”,这样的表达式叫做后缀表达式。后缀表达式方便计算机的理解与运算。对比两者得到区别我们会发现前缀表达式与后缀表达式的最大区别是不存在括号与运算符号均置于数字后侧。
如何将中缀表达式转化为后缀表达式
首先先指定运算符的优先级:
即开平方 > 乘除 > 加减 > 左括号。这里我们可以引入一个switch
结构进行判断,int
型数据返回当前符号优先级,已此来完成优先级运算符的判断。
运算符号 | 优先级别 |
---|---|
加(+) | 1 |
减(-) | 1 |
乘(+ 、 *) | 2 |
除(÷ 、\) | 2 |
开平方( sqrt() ) | 3 |
左括号与其他 | 0 |
其次是转换方法上,引入两个栈数字栈(nu_stack)
与符号栈(op_stack)
。
转化时遵循以下规则:
- 遇到数字时将数字压入
nu_stack
,等待运算。 - 遇到是运算符号时,尝试将符号压入
op_stack
。①当栈空时直接压入;②当非栈空时,比较栈顶(top()
)元素的运算优先级是否高于当前尝试入栈的运算符号。如果栈顶运算符高于当前尝试入栈的运算符,则不断将栈内的运算符号弹出栈直至栈顶运算优先级小于或等于尝试入栈的运算符。如果栈顶运算符优先级小于或等于当前尝试入栈的运算符,则直接入栈。 - 当遇到是左括号时,左括号无条件直接入栈。
- 当遇到右括号时,不断弹出运算符号,直到遇到左括号为止。
代码实现
#include<iostream>
#include<stack>
#include<string>
using namespace std;
int judgeLevel(char op) { //判断运算符的优先级
switch (op){
case '+':
case '-':
return 1;
case '*':
case '/':
return 2;
case 's':
return 3;
default:
return 0;
}
}
int calculate(int num_1, char ope, int num_2) { //双目运算的计算函数
switch (ope) {
case '+':
return num_1 + num_2;
case '-':
return num_1 - num_2;
case '*':
return num_1 * num_2;
case '/':
return num_1 / num_2; //精度可能丢失
default:
return 0;
}
}
int calculate(int num_1) { //单目运算符的计算函数
return sqrt(num_1); //计算结果为double,返回int,存在精度丢失
}
int changeFormula(string formula) {
stack<int> number = stack<int>(); //创建一个数字栈
stack<char> ope = stack<char>(); //创建一个符号栈
char c; //用于暂时存储一个字符
int num_1 = 0; int num_2 = 0; //初始化两个数,方便后面计算使用
for (int i = 0; i < formula.length(); i++) {
if ('0' <= formula[i] && formula[i] <= '9') { //如果当前是一个数字
c = formula[i];
number.push(atoi(&c)); //将数字推入数字栈
}
else { //如果不是数字
switch (formula[i]) {
case '(': //如果是左括号
ope.push(formula[i]);
break;
case ')': //如果是右括号
while (ope.top() != '(') { //一直弹出运算符直至左括号
c = ope.top(); ope.pop(); //获取一个运算符
if (c == 's') { //如果是单目运算符,如阶乘、开平方、正弦函数等等,进行单目运算符运算,弹出一个数字即可
num_1 = number.top(); //获取栈顶数字
number.pop(); //弹出栈顶数字
number.push(calculate(num_1)); //压入运算结果
}
else { //加减乘除等等需要两个数字
num_2 = number.top(); //获取栈顶数字(获取第一个数字)
number.pop(); //弹出栈顶数字
num_1 = number.top(); //获取栈顶数字(获取第二个数字)
number.pop(); //弹出栈顶数字
number.push(calculate(num_1, c, num_2)); //计算结果压入栈
}
}
ope.pop(); //将左括号弹出栈
break;
default: //如果是运算符
if (ope.empty()) { //如果是空栈,直接压入运算符
ope.push(formula[i]);
break;
}
if (judgeLevel(formula[i]) < judgeLevel(ope.top())) {
//如果不是空栈,对比栈顶运算符和要压入的运算符二者优先级
//如果当前栈顶的运算符比要压入的运算符优先级低,弹出运算符直至空栈或小于或同等优先级
while (judgeLevel(formula[i]) < judgeLevel(ope.top()) && ope.empty() != true) {
c = ope.top(); ope.pop();
if (c == 's') {
num_1 = number.top();
number.pop();
number.push(calculate(num_1));
}
else {
num_2 = number.top();
number.pop();
num_1 = number.top();
number.pop();
number.push(calculate(num_1, c, num_2));
}
}
ope.push(formula[i]); //将要压入栈的运算符压入
}
else {
//如果栈顶的运算符小于或等于要压入的运算符的优先级,直接压入栈
ope.push(formula[i]); //压入栈
}
break;
}
}
}
//完成遍历,检查是否已经将所有运算执行完成,即运算符栈是否为空
while (!ope.empty()) { //如果运算符栈没有空,清空运算符栈
c = ope.top(); ope.pop();
if (c == 's') {
num_1 = number.top();
number.pop();
number.push(calculate(num_1));
}
else {
num_2 = number.top();
number.pop();
num_1 = number.top();
number.pop();
number.push(calculate(num_1, c, num_2));
}
}
return number.top(); //此时数字栈中只有一个数字,就是结果,返回结果
}
int main() {
cout << changeFormula("2+3*5-(6+1)");
return 0;
}