1、栈的概念
栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。总的来说就是LIFO(Last In First Out);
代码:
#pragma once
/*
*Copyright© 中国地质大学(武汉) 信息工程学院
*All right reserved.
*
*文件名称:Stack.h
*摘 要:编写栈算法
*
*当前版本:1.0
*作 者:邵玉胜
*完成日期:2018-12-28
*/
#ifndef STACK_H_
#define STACK_H_
#include
//结点结构体,双向栈
template
struct StackNode
{
T _tData; //数据域
StackNode
StackNode
StackNode(StackNode
StackNode
this->_pNext = nullptr;
this->_pLast = nullptr;
}
StackNode(T data,StackNode
StackNode
this->_tData = data;
this->_pNext = next;
this->_pLast = last;
}
};
template
class Stack {
private:
StackNode
StackNode
int _iConuntOfElement; //结点数量
public:
Stack(); //构造函数
Stack(Stack
~Stack(); //析构函数
bool IsEmpty(); //判断栈是否为空
void MakeEmpty(); //将栈中的元素全部删除
void Put(const T data); //顶端插入数据
int Size() { return _iConuntOfElement; } //返回栈中的结点数
void GetTop(T& data); //获取顶端结点
void Pop(T& data); //顶端弹出结点,并将元素传至参数中
void Traverse(); //逆序栈中的结点
void DisPlay(bool forward = true); //输出函数,默认正向输出
};
//构造函数,为栈顶和栈底分配内存
template
Stack
_pTop = _pBottom = nullptr;
this->_iConuntOfElement = 0;
}
//拷贝构造函数
//缺少此函数,在传参与析构的时候容易出问题
template
Stack
StackNode
while (pCur) { //遍历本对象的结点
T data = pCur->_tData; //依此取出结点值
copy.Put(data); //插入到copy栈中
pCur = pCur->_pNext;
}
}
//析构函数
template
Stack
MakeEmpty(); //释放结点内存
this->_pTop = this->_pBottom = nullptr; //将指针指向空,避免出现野指针
}
//判断栈是否为空
template
bool Stack
if (!this->_pTop) //如果栈对象没有头节点,那么栈就为空
return true;
return false;
}
//将栈中的元素全部删除
template
void Stack
StackNode
while (_pTop) { //循环依此从顶端删除
pDel = this->_pTop;
this->_pTop = _pTop->_pNext;
delete pDel;
}
_iConuntOfElement = 0; //栈结点数量置零
}
//顶端插入数据
template
void Stack
StackNode
if (!newData) { //如果内存分配错误
std::cerr << "内存分配错误!" << std::endl;
exit(-1);
}
if (this->IsEmpty()) { //如果插入的是第一个结点
newData->_pLast = nullptr; //将这个结点的指向上一下一结点的指针都赋空值
newData->_pNext = nullptr;
this->_pTop = newData; //将栈顶和栈底指针都指向这个结点
this->_pBottom = newData;
++this->_iConuntOfElement; //节点数加1
return;
}
this->_pTop->_pLast = newData; //栈顶的上一结点指针指向新结点
newData->_pNext = this->_pTop; //将新结点的下一结点指针指向栈顶
this->_pTop = newData; //新结点作为栈顶
++this->_iConuntOfElement;
}
//获取顶端结点
template
void Stack
if (this->IsEmpty()) //栈为空,直接返回
return;
data = this->_pTop->_tData; //获取栈顶元素,赋值给返回参数
return;
}
//顶端弹出元素,并将元素传至参数中
template
void Stack
if (this->IsEmpty()) //栈为空,直接返回
return;
data = this->_pTop->_tData; //先取出栈顶的值
StackNode
if (this->_pTop->_pNext) { //如果有后继结点,就将后继结点的上一个指针指向空
this->_pTop->_pNext->_pLast = nullptr;
this->_pTop = this->_pTop->_pNext;
delete pDel; //释放原栈顶内存
--this->_iConuntOfElement; //栈结点数量递减
return;
}
delete pDel; //如果就只有一个结点,直接释放原栈顶的空间
this->_pTop = this->_pBottom = nullptr; //没有后继结点,释放栈顶空间,将指针指向空,避免出现野指针
--this->_iConuntOfElement; //栈结点数量递减
return;
}
//逆序栈中的结点
template
void Stack
StackNode
StackNode
while (pCur) { //循环将栈中结点的前驱与后继指针对调
pTmp = pCur->_pLast;
pCur->_pLast = pCur->_pNext;
pCur->_pNext = pTmp;
pCur = pCur->_pLast; //这个是很值得细细品味的
}
//将栈顶与栈顶指针对调
pTmp = this->_pTop;
this->_pTop = this->_pBottom;
this->_pBottom = pTmp;
return;
}
//输出函数
template
void Stack
StackNode
if(forward == true)
pCur = this->_pTop;
else
pCur = this->_pBottom;
while (pCur) { //如果当前指针不为空,就一直循环遍历
std::cout << pCur->_tData; //为了照顾
//if (iCount % 10 == 0) //每隔十个换一行,以免输出的太长
// std::cout << std::endl;
if (forward == true)
pCur = pCur->_pNext;
else
pCur = pCur->_pLast;
}
std::cout << std::endl;
}
#endif // !STACK_H_
由于栈的后进先出的特性,使得栈有很多的应用,下面就一一举例:
2、栈的应用之-数制转换
十进制数N和其他d进制数的转换是计算机实现计算的基本问题,其解决方法很多,其中一个简单算法基于下列原理:
N = (N div d) * d + N mod d(其中:div为整除运算,mod为求余运算)
例如:将十进制的121转化为二进制的过程为:
代码:
//进制转换函数
//十进制转换为其他低进制,如二进制,八进制,默认是二进制
//注意这个函数仅适用于整数
void DecConvert(Stack
const int decimal,const int radix) {
if (radix < 2 && radix > 10) { //先判断参数合不合理
std::cout << "进制转换函数参数不合理!" << std::endl;
return;
}
result.MakeEmpty(); //先将用于结果返回的栈参数置空
int iQuotient = decimal; //商
int iRemainder; //余数
while (iQuotient) { //商为0的时候结束循环
iRemainder = iQuotient % radix; //求余
result.Put(iRemainder); //将余数装入结果的栈中
iQuotient /= radix; //求商
}
}
3、栈的应用之-括号匹配
假设表达式中包含三种括号:圆括号、方括号和花括号,并且它们可以任意嵌套。例如{[()]()[{}]}或[{()}([])]等为正确格式,而{[}()]或[({)]为不正确的格式。那么怎么检测表达式是否正确呢?
这个问题可以用“期待的急迫程度”这个概念来描述。对表达式中的每一个左括号都期待一个相应的右括号与之匹配,表达式中越迟出现并且没有得到匹配的左括号期待匹配的程度越高。不是期待出现的右括号则是不正确的。它具有天然的后进先出的特点。
于是我们可以设计算法:算法需要一个栈,在读入字符的过程中,如果是左括号,则直接入栈,等待相匹配的同类右括号;如果是右括号,且与当前栈顶左括号匹配,则将栈顶左括号出栈,如果不匹配则属于不合法的情况。另外,如果碰到一个右括号,而堆栈为空,说明没有左括号与之匹配,则非法。那么,当字符读完的时候,如果是表达式合法,栈应该是空的,如果栈非空,那么则说明存在左括号没有相应的右括号与之匹配,也是非法的情况。
代码:
//括号匹配函数
//注意此处的括号匹配使用的时英文括号
//检验括号是否匹配,该方法使用“期待的紧迫程度”这个概念来描述的
//可能出现不匹配的情况
//1、到来的有括弧非是所“期待”的
//2、到来的是不速之客(左括弧多了)
//3、直到结束也没有到来所“期待”的
//设计思想:1、凡是出现左括弧就让他进栈
//2、若出现右括弧,则检查栈是否为空,若为空,就表明右括弧多了
//否则和栈顶元素匹配,匹配成果,则左括弧出栈,否则,匹配失败!
//3、表达式检验结束时,检查栈是否为空,不为空,说明左括号多了
void ParMatching(std::string str) {
//先定义一些括号常量
const char LCURVES = '(';
const char RCURVES = ')';
const char LBRAKET = '[';
const char RBRAKET = ']';
const char LBRACE = '{';
const char RBRACE = '}';
Stack
char chPop; //保存弹出的括号
for (int i = 0; i < str.length(); i++) {
switch (str[i])
{
case LCURVES: //凡是出现左括弧就让他进栈
case LBRAKET:
case LBRACE:
stackMatch.Put(str[i]);
break;
case RCURVES: //若出现右括弧
if (!stackMatch.IsEmpty()) { //则检查栈是否为空,否则和栈顶元素匹配
stackMatch.GetTop(chPop);
if (chPop == LCURVES) { //匹配成果,则左括弧出栈
stackMatch.Pop(chPop);
std::cout << "()匹配成功!n";
break;
}
}
std::cout << ")匹配失败!n"; //否则,就表明右括弧多了
break;
case RBRAKET:
if (!stackMatch.IsEmpty()) {
stackMatch.GetTop(chPop);
if (chPop == LBRAKET) {
stackMatch.Pop(chPop); //弹出
std::cout << "[]匹配成功!n";
break;
}
}
std::cout << "]匹配失败!n";
break;
case RBRACE:
if (!stackMatch.IsEmpty()) {
stackMatch.GetTop(chPop);
if (chPop == LBRACE) {
stackMatch.Pop(chPop); //弹出
std::cout << "{}匹配成功!n";
break;
}
}
std::cout << "}匹配失败!n";
break;
default:
break;
}
}
//将栈中没有匹配的左括号给弹出来
for (int i = 0; i < stackMatch.Size(); i++) {
stackMatch.Pop(chPop);
std::cout << chPop << "匹配失败!n";
}
return;
}
4、栈的应用之-行编辑程序
一个简单的行编辑程序的功能是:接受用户从终端输入的程序或数据,并存入用户的数据区。由于用户在终端上进行输入时,不能保证不出差错,因此,若在行编辑程序中“每接受一个字符即存入用户区”的做法显然是不恰当的。
较好的做法是,设立一个输入缓冲区,用以接收用户输入的一行字符,然后逐行存入用户数据区。允许用户输入出差错,
并在发现有误时及时改正。
例如:当用户发现刚刚建入的一个字符是错的时,可补进一个退格符“#”,以表示前一个字符无效;如果发现当前键入的行内差错较多或难以补救,则可以输入一个退格符“@”,以表示当前行中的字符均无效。例如,假设从终端接受了这两行字符:
whil##ilr#e(s#*s)
outcha@putchar(*s=#++)
则实际有效的是下列两行:
while(*s)
putchar(*s++)
代码:
//行编辑程序问题
//设立一个输入缓冲区,用以接受用户输入的一行字符,
//然后逐行存入用户数据区。允许用户输入出差错,并在发现的时候可以及时改正。
//例如,当用户刚刚键入的一个字符是错误的时候,可以补进一个退格符“#”,
//以表示前一个字符无效;如果发现当前键入的行内差错比较多或难以补救,
//则可以键入一个退行符“@”,以表示当前行中的字符均无效。
void LineEditing() {
Stack
char chInput; //接收输入的字符
char chTmp; //临时字符变量,用于保存弹出的字符
chInput = getchar(); //获取输入的字符
while (chInput != EOF) { //EOF为全文结束符,遇到他则不在等待输入,ctrl+Z
stackResult.MakeEmpty();
while (chInput != EOF && chInput != 'n') { //如果没有输入回车和全文结束符
switch (chInput)
{
case '#': //输错一个字符
stackResult.Pop(chTmp); //弹出这个字符
break;
case '@': //输出一行字符
stackResult.MakeEmpty(); //情况栈
break;
default:
stackResult.Put(chInput);
break;
}
chInput = getchar();
}
stackResult.DisPlay(false);
chInput = getchar();
}
}
5、栈的应用之表达式求值
表达式求值可以认为是栈的典型应用之一了,如果想弄明白表达式求值的方法这里一两句是介绍不清楚的,主要需要了解表达式三种表示方法(中缀、前缀、后缀)以及为什么要选后缀最为最后参与运算的表示方式(读者可以自行了解,这里不做介绍)。既然是用后缀作为周会残云运算的表示方式,而我们平时使用的右采用的中缀表达式,因此,欲求中缀表达式的值,需要将中缀表达式的值转换为后缀表达式,我将主要的参考代码放下下面,里面由较为详细的注释说明。
代码:
//辅助函数:运算符优先级函数,返回符号的优先级(+-x/()六种)
int OperatorPriority(char chOperator) {
int result;
switch (chOperator)
{
case '(': //左括号的优先级最大为6
result = 6;
break;
case ')': //右括号的优先级最小,为1
result = 1;
break;
case '+': //+-的优先级为2
case '-':
result = 2;
break;
case '*': //乘除的优先级为3
case '/':
result = 3;
break;
case '#': //栈底放一个‘#’方便运算,比所有运算符都笑
result = 0;
break;
default:
result = -1; //如果是除了数字与上诉运算符的其他字符,就默认返回-1
break;
}
return result;
}
//1、先将原表达式变成后缀表达式
//2、在利用栈将后缀表达式求值
float ExpressionEvaluating(std::string strExpression) {
Stack
Stack
Stack
char chTopOfStack; //运算符栈的顶端操作符
float fReault; //保存结果
stkChOperator.Put('#'); //往栈底放入‘#’,其比任何运算符的优先级都小
for (int i = 0; i < strExpression.length(); i++) {//对字符串中的字符进行分析
char chRead = strExpression[i];
if (chRead >= '0' && chRead <= '9') //如果读出的是数字,直接放到后缀栈
stkChPostfix.Put(chRead);
else //读出的是其他字符的话
{
if (OperatorPriority(chRead) == -1) {//如果读出的不是数字,也不是操作符
std::cerr << "表达式有误!n";
exit(-1);
}
while (true) {
stkChOperator.GetTop(chTopOfStack);
if (OperatorPriority(chTopOfStack) >=
OperatorPriority(chRead)) {
stkChOperator.Pop(chTopOfStack); //弹出顶端操作符
if (chTopOfStack != '(') //'('是不入后缀表达式的栈的
stkChPostfix.Put(chTopOfStack); //向后缀表达式的栈读入运算符栈顶端操作符
}
else
break;
}
if (chRead != ')') //')'也是不吐后缀表达式的栈的
stkChOperator.Put(chRead); //向运算符栈中装入读取的操作符
}
}
while (stkChOperator.Size() > 1) { //将操作符栈中的剩余操作符弹出放入后缀表达式
stkChOperator.Pop(chTopOfStack);
stkChPostfix.Put(chTopOfStack);
}
stkChPostfix.Traverse(); //将后缀表达式栈逆序
//开始由后缀表达式计算结果
while (stkChPostfix.Size() > 0) { //一直弹出后缀表达式栈顶值
stkChPostfix.Pop(chTopOfStack);
if (chTopOfStack >= '0' && chTopOfStack <= '9')
stkReult.Put(float(chTopOfStack) - 48.0); //0-9的ascii码是 48-58
else //如果弹出的是运算结果
{
float fFirst;
float fSencond;
stkReult.Pop(fSencond);
stkReult.Pop(fFirst);
switch (chTopOfStack)
{
case '+':
stkReult.Put(fFirst + fSencond);
break;
case '-':
stkReult.Put(fFirst - fSencond);
break;
case '*':
stkReult.Put(fFirst * fSencond);
break;
case '/':
stkReult.Put(fFirst / fSencond);
break;
default:
break;
}
}
}
stkReult.GetTop(fReault); //获取结果
return fReault; //返回结果
}
此外,本人还利用栈和MFC做了几个简易的计算器,可以实现加减乘除、指数、开方等运算,支持连续输入。有需要的可以自行下载,以供相互交流,计算机的界面如下:
计算器代码下载地址:
MFC+栈实现简易计算器
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。