数据结构之-链式栈及其常见应用进制转换、括号匹配、行编辑程序、表达式求值等

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* _pNext; //指针域,指向下一结点

StackNode* _pLast; //指针域,指向上一结点(为了行编辑器函数的实现)

StackNode(StackNode* next = nullptr,

StackNode* last = nullptr) { //用指针构造函数

this->_pNext = nullptr;

this->_pLast = nullptr;

}

StackNode(T data,StackNode* next = nullptr,

StackNode* last = nullptr) { //用指针构造函数

this->_tData = data;

this->_pNext = next;

this->_pLast = last;

}

};

template

class Stack {

private:

StackNode* _pTop; //栈顶指针

StackNode* _pBottom; //栈底指针,为了方便行编辑器使用

int _iConuntOfElement; //结点数量

public:

Stack(); //构造函数

Stack(Stack& copy); //构造函数

~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::Stack() {

_pTop = _pBottom = nullptr;

this->_iConuntOfElement = 0;

}

//拷贝构造函数

//缺少此函数,在传参与析构的时候容易出问题

template

Stack::Stack(Stack& copy) {

StackNode* pCur = this->_pTop; //建立指针,用于遍历本对象中的结点

while (pCur) { //遍历本对象的结点

T data = pCur->_tData; //依此取出结点值

copy.Put(data); //插入到copy栈中

pCur = pCur->_pNext;

}

}

//析构函数

template

Stack::~Stack() {

MakeEmpty(); //释放结点内存

this->_pTop = this->_pBottom = nullptr; //将指针指向空,避免出现野指针

}

//判断栈是否为空

template

bool Stack::IsEmpty() {

if (!this->_pTop) //如果栈对象没有头节点,那么栈就为空

return true;

return false;

}

//将栈中的元素全部删除

template

void Stack::MakeEmpty(){

StackNode* pDel = nullptr; //建立临时结点指针,用于释放结点内存

while (_pTop) { //循环依此从顶端删除

pDel = this->_pTop;

this->_pTop = _pTop->_pNext;

delete pDel;

}

_iConuntOfElement = 0; //栈结点数量置零

}

//顶端插入数据

template

void Stack::Put(const T data) {

StackNode* newData = new StackNode(data); //为新结点分配内存,新结点的并确定结点指针指向

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::GetTop(T& data) {

if (this->IsEmpty()) //栈为空,直接返回

return;

data = this->_pTop->_tData; //获取栈顶元素,赋值给返回参数

return;

}

//顶端弹出元素,并将元素传至参数中

template

void Stack::Pop(T& data) {

if (this->IsEmpty()) //栈为空,直接返回

return;

data = this->_pTop->_tData; //先取出栈顶的值

StackNode* pDel = this->_pTop;

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::Traverse() {

StackNode* pCur = this->_pTop;

StackNode* pTmp = nullptr; //临时指针,循环要用

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::DisPlay(bool forward = true) {

StackNode* pCur;

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& result,

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 stackMatch; //定义一个栈对象,用于装入弹出左括号

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 stackResult; //建立临时栈,用于存储输入字符的缓存

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 stkChOperator; //建立一个用于装运算符字符的栈

Stack stkChPostfix; //建立一个用于装后缀表达式的栈

Stack stkReult; //存储运算结果的栈

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 举报,一经查实,本站将立刻删除。

相关推荐


项目中经常遇到CSV文件的读写需求,其中的难点主要是CSV文件的解析。本文会介绍CsvHelper、TextFieldParser、正则表达式三种解析CSV文件的方法,顺带也会介绍一下CSV文件的写方法。 CSV文件标准 在介绍CSV文件的读写方法前,我们需要了解一下CSV文件的格式。 文件示例 一
简介 本文的初衷是希望帮助那些有其它平台视觉算法开发经验的人能快速转入Halcon平台下,通过文中的示例开发者能快速了解一个Halcon项目开发的基本步骤,让开发者能把精力完全集中到算法的开发上面。 首先,你需要安装Halcon,HALCON 18.11.0.1的安装包会放在文章末尾。安装包分开发和
这篇文章主要简单记录一下C#项目的dll文件管理方法,以便后期使用。 设置dll路径 参考C#开发奇技淫巧三:把dll放在不同的目录让你的程序更整洁中间的 方法一:配置App.config文件的privatePath : &lt;runtime&gt; &lt;assemblyBinding xml
在C#中的使用JSON序列化及反序列化时,推荐使用Json.NET——NET的流行高性能JSON框架,当然也可以使用.NET自带的 System.Text.Json(.NET5)、DataContractJsonSerializer、JavaScriptSerializer(不推荐)。
事件总线是对发布-订阅模式的一种实现,是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。&#xA;EventBus维护一个事件的字典,发布者、订阅者在事件总线中获取事件实例并执行发布、订阅操作,事件实例负责维护、执行事件处理程序。
通用翻译API的HTTPS 地址为https://fanyi-api.baidu.com/api/trans/vip/translate,使用方法参考通用翻译API接入文档 。&#xA;请求方式可使用 GET 或 POST 方式(Content-Type 请指定为:application/x-www-for
词云”由美国西北大学新闻学副教授、新媒体专业主任里奇·戈登(Rich Gordon)于2006年最先使用,是通过形成“关键词云层”或“关键词渲染”,对文本中出现频率较高的“关键词”的视觉上的突出。词云图过滤掉大量的文本信息,使浏览者只要一眼扫过文本就可以领略文本的主旨。&#xA;网上大部分文章介绍的是使用P
微软在.NET中对串口通讯进行了封装,我们可以在.net2.0及以上版本开发时直接使用SerialPort类对串口进行读写操作。&#xA;为操作方便,本文对SerialPort类做了一些封装,暂时取名为**SerialPortClient**。
简介 管道为进程间通信提供了平台, 管道分为两种类型:匿名管道、命名管道,具体内容参考.NET 中的管道操作。简单来说,匿名管道只能用于本机的父子进程或线程之间,命名管道可用于远程主机或本地的任意两个进程,本文主要介绍命名管道的用法。 匿名管道在本地计算机上提供进程间通信。 与命名管道相比,虽然匿名
目录自定义日志类NLog版本的日志类Serilog版本的日志类 上个月换工作,新项目又要重新搭建基础框架,把日志实现部分单独记录下来方便以后参考。 自定义日志类 代码大部分使用ChatGPT生成,人工进行了测试和优化,主要特点: 线程安全,日志异步写入文件不影响业务逻辑 支持过期文件自动清理,也可自
[TOC] # 原理简介 本文参考[C#/WPF/WinForm/程序实现软件开机自动启动的两种常用方法](https://blog.csdn.net/weixin_42288432/article/details/120059296),将里面中的第一种方法做了封装成**AutoStart**类,使
简介 FTP是FileTransferProtocol(文件传输协议)的英文简称,而中文简称为“文传协议”。用于Internet上的控制文件的双向传输。同时,它也是一个应用程序(Application)。基于不同的操作系统有不同的FTP应用程序,而所有这些应用程序都遵守同一种协议以传输文件。 FTP
使用特性,可以有效地将元数据或声明性信息与代码(程序集、类型、方法、属性等)相关联。 将特性与程序实体相关联后,可以在运行时使用反射这项技术查询特性。&#xA;在 C# 中,通过用方括号 ([]) 将特性名称括起来,并置于应用该特性的实体的声明上方以指定特性。
# 简介 主流的识别库主要有ZXing.NET和ZBar,OpenCV 4.0后加入了QR码检测和解码功能。本文使用的是ZBar,同等条件下ZBar识别率更高,图片和部分代码参考[在C#中使用ZBar识别条形码](https://www.cnblogs.com/w2206/p/7755656.htm
C#中Description特性主要用于枚举和属性,方法比较简单,记录一下以便后期使用。 扩展类DescriptionExtension代码如下: using System; using System.ComponentModel; using System.Reflection; /// &lt;
本文实现一个简单的配置类,原理比较简单,适用于一些小型项目。主要实现以下功能:保存配置到json文件、从文件或实例加载配置类的属性值、数据绑定到界面控件。&#xA;一般情况下,项目都会提供配置的设置界面,很少手动更改配置文件,所以选择以json文件保存配置数据。
前几天用SerialPort类写一个串口的测试程序,关闭串口的时候会让界面卡死。网上大多数方法都是定义2个bool类型的标记Listening和Closing,关闭串口和接受数据前先判断一下。我的方法是DataReceived事件处理程序用this.BeginInvoke()更新界面,不等待UI线程
约束告知编译器类型参数必须具备的功能。 在没有任何约束的情况下,类型参数可以是任何类型。 编译器只能假定 System.Object 的成员,它是任何 .NET 类型的最终基类。 如果客户端代码使用不满足约束的类型,编译器将发出错误。 通过使用 where 上下文关键字指定约束。&#xA;最常用的泛型约束为
protobuf-net是用于.NET代码的基于契约的序列化程序,它以Google设计的“protocol buffers”序列化格式写入数据,适用于大多数编写标准类型并可以使用属性的.NET语言。&#xA;protobuf-net可通过NuGet安装程序包,也可直接访问github下载源码:https:/
工作中经常遇到需要实现TCP客户端或服务端的时候,如果每次都自己写会很麻烦且无聊,使用SuperSocket库又太大了。这时候就可以使用SimpleTCP了,当然仅限于C#语言。&#xA;SimpleTCP是一个简单且非常有用的 .NET 库,用于处理启动和使用 TCP 套接字(客户端和服务器)的重复性任务