【数据结构(邓俊辉)学习笔记】栈与队列01——栈接口与应用

news2024/12/22 19:14:23

文章目录

  • 0. 概述
  • 1. 操作与接口
  • 2. 操作实例
  • 3. 实现
  • 4. 栈与递归
  • 5. 应用
    • 5.1 逆序输出
      • 5.1.1 进制转换
        • 5.1.1.1 思路
        • 5.1.1.2 算法实现
    • 5.2 递归嵌套
      • 5.2.1 栈混洗
        • 5.2.1.1 混洗
        • 5.2.1.2 计数
        • 5.2.1.3 甄别
      • 5.2.2 括号匹配
        • 5.2.2.1 构思
        • 5.2.2.2 实现
        • 5.2.2.3 实例
    • 5.3 延迟缓冲
      • 5.3.1 中缀表达式
        • 5.3.1.1 表达式求值
        • 5.3.1.2 优先级表
        • 5.3.1.3 求值算法
      • 5.3.2 逆波兰表达式(后缀表达式)
        • 5.3.2.1 RPN
        • 5.3.2.2 RPN实例
        • 5.3.2.3 infix 到postfix 转换

0. 概述

介绍下栈的接口与应用。

1. 操作与接口

在这里插入图片描述

2. 操作实例

在这里插入图片描述

3. 实现

在这里插入图片描述
基于向量

#include "Vector/Vector.h" //以向量为基类,派生出栈模板类
template <typename T> 
class Stack: public Vector<T> { //将向量的首/末端作为栈底/顶
public: //原有接口一概沿用
   void push ( T const& e ) { insert ( e ); } //入栈:等效于将新元素作为向量的末元素插入
   T pop() { return remove ( size() - 1 ); } //出栈:等效于删除向量的末元素
   T& top() { return ( *this ) [size() - 1]; } //取顶:直接返回向量的末元素
};

基于列表

#include "List/List.h" //以列表为基类,派生出栈模板类
template <typename T> 
class Stack: public List<T> { //将列表的首/末端作为栈底/顶
public: //原有接口一概沿用
   void push ( T const& e ) { insertAsLast ( e ); } //入栈:等效于将新元素作为列表的末元素插入
   T pop() { return remove ( last() ); } //出栈:等效于删除列表的末元素
   T& top() { return last()->data; } //取顶:直接返回列表的末元素
};

4. 栈与递归

在这里插入图片描述

5. 应用

典型应用场合
在这里插入图片描述

5.1 逆序输出

5.1.1 进制转换

5.1.1.1 思路

在这里插入图片描述
在这里插入图片描述

5.1.1.2 算法实现
  1. 递归实现
void convert ( Stack<char>& S, __int64 n, int base ) { //十进制数n到base进制的转换(递归版)
	static char digit[] //0 < n, 1 < base <= 16,新进制下的数位符号,可视base取值范围适当扩充
	= { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
	if ( 0 < n ) { //在尚有余数之前,不断
		convert ( S, n / base, base ); //通过递归得到所有更高位
		S.push ( digit[n % base] ); //输出低位
	}
} //新进制下由高到低的各数位,自顶而下保存于栈S中
  1. 迭代实现

优化点:
这里的静态数位符号表在全局只需保留一份,但与一般的递归函数一样,该函数在递归调用栈中的每一帧都仍需记录参数S、n和base。将它们改为全局变量固然可以节省这部分空间,但依然不能彻底地避免因调用栈操作而导致的空间和时间消耗。

改写成迭代版将空间消耗降至O(1)。

void convert ( Stack<char>& S, __int64 n, int base ) { //十进制数n到base进制的转换(迭代版)
	static char digit[] //0 < n, 1 < base <= 16,新进制下的数位符号,可视base取值范围适当扩充
	= { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
	while ( n > 0 ) { //由低到高,逐一计算出新进制下的各数位
		int remainder = ( int ) ( n % base ); S.push ( digit[remainder] ); //余数(当前位)入栈
		n /= base; //n更新为其对base的除商
	}
} //新进制下由高到低的各数位,自顶而下保存于栈S中

5.2 递归嵌套

5.2.1 栈混洗

5.2.1.1 混洗

栈混洗就是按照某种约定的规则对栈中元素进行重新的排列。
在这里插入图片描述
上图采用约定尖括号<表示栈顶,方括号]表示栈底。

在遵守以上规则的前提下,同一输入序列完全可以导出不同的栈混洗序列。
在这里插入图片描述
n个元素的栈混洗总数不会超过全排列n!。

一般地对于长度为n的输入序列,每一栈混洗都对应于由栈S的n次push和n次pop构成的某一合法操作序列比如[ 3, 2,4, 1> 
即对应于操作序列: 
        {push, push, push, pop, pop, push, pop, pop } 
反之,由n次push和n次pop构成的任何操作序列,只要满足“任一前缀中的push不少于pop”这一限制,则该序列也必然对应于
某个栈混洗。
5.2.1.2 计数

在这里插入图片描述
假定输入栈A中共有n个元素,自顶向下依次编号为1,2,3 … n。将注意力放在第一个元素1上,它将被首次推入中转栈S中,若1号元素被弹出,则栈S就是空栈,此时在栈B中包括刚推入的1号元素,累计共有k个元素。那么栈A中就应该还留存有最后的n-k个元素。此时B中k个元素和A中n-k个元素它们的栈混洗是相互独立的,故1号元素作为第k个元素被推入B中的情况,累计栈混洗总数如下
在这里插入图片描述

5.2.1.3 甄别

在这里插入图片描述
在这里插入图片描述

5.2.2 括号匹配

5.2.2.1 构思

在这里插入图片描述

5.2.2.2 实现

算法思想:只要将push、pop操作分别与左、右括号相对应,则长度为n的栈混洗,必然与由n对括号组成的合法表达式彼此对应。 比如,栈混洗[ 3, 2, 4, 1 >对应于表达式"( ( ( ) ) ( ) )"。按照这一理解,借助栈结构,只需扫描一趟表达式,即可在线性时间内,判定其中的括号是否匹配。

bool paren ( const char exp[], int lo, int hi ) { //表达式括号匹配检查,可兼顾三种括号
	Stack<char> S; //使用栈记录已发现但尚未匹配的左括号
	for ( int i = lo; i <= hi; i++ ) /* 逐一检查当前字符 */
	switch ( exp[i] ) { //左括号直接进栈;右括号若与栈顶失配,则表达式必不匹配
		case '(': case '[': case '{': S.push ( exp[i] ); break;
		case ')': if ( ( S.empty() ) || ( '(' != S.pop() ) ) return false; break;
		case ']': if ( ( S.empty() ) || ( '[' != S.pop() ) ) return false; break;
		case '}': if ( ( S.empty() ) || ( '{' != S.pop() ) ) return false; break;
		default: break; //非括号字符一律忽略
	}
	return S.empty(); //整个表达式扫描过后,栈中若仍残留(左)括号,则不匹配;否则(栈空)匹配
}
5.2.2.3 实例

在这里插入图片描述

5.3 延迟缓冲

在一些应用问题中,输入可分解为多个单元并通过迭代依次扫描处理,但过程中的各步计算往往滞后于扫描的进度,需要待到必要的信息已完整到一定程度之后,才能作出判断并实施计算。在这类场合,栈结构则可以扮演数据缓冲区的角色。

中缀表达式算法的求解过程同逆波兰表达式算法,下面介绍下。

5.3.1 中缀表达式

5.3.1.1 表达式求值

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.3.1.2 优先级表

将不同运算符之间的运算优先级关系,描述为一张二维表格。
在这里插入图片描述

#define N_OPTR 9 //运算符总数
typedef enum { ADD, SUB, MUL, DIV, POW, FAC, L_P, R_P, EOE } Operator; //运算符集合
//加、减、乘、除、乘方、阶乘、左括号、右括号、起始符与终止符

const char pri[N_OPTR][N_OPTR] = { //运算符优先等级 [栈顶] [当前]
   /*              |-------------------- 当 前 运 算 符 --------------------| */
   /*              +      -      *      /      ^      !      (      )      \0 */
   /* --  + */    '>',   '>',   '<',   '<',   '<',   '<',   '<',   '>',   '>',
   /* |   - */    '>',   '>',   '<',   '<',   '<',   '<',   '<',   '>',   '>',
   /* 栈  * */    '>',   '>',   '>',   '>',   '<',   '<',   '<',   '>',   '>',
   /* 顶  / */    '>',   '>',   '>',   '>',   '<',   '<',   '<',   '>',   '>',
   /* 运  ^ */    '>',   '>',   '>',   '>',   '>',   '<',   '<',   '>',   '>',
   /* 算  ! */    '>',   '>',   '>',   '>',   '>',   '>',   ' ',   '>',   '>',
   /* 符  ( */    '<',   '<',   '<',   '<',   '<',   '<',   '<',   '=',   ' ',
   /* |   ) */    ' ',   ' ',   ' ',   ' ',   ' ',   ' ',   ' ',   ' ',   ' ',
   /* -- \0 */    '<',   '<',   '<',   '<',   '<',   '<',   '<',   ' ',   '='
};
5.3.1.3 求值算法
double evaluate ( char* S, char* RPN ) { //对(已剔除白空格的)表达式S求值,并转换为逆波兰式RPN
   Stack<double> opnd; Stack<char> optr; //运算数栈、运算符栈 /*DSA*/任何时刻,其中每对相邻元素之间均大小一致
   /*DSA*/ char* expr = S;
   optr.push ( '\0' ); //尾哨兵'\0'也作为头哨兵首先入栈
   while ( !optr.empty() ) { //在运算符栈非空之前,逐个处理表达式中各字符
      if ( isdigit ( *S ) ) { //若当前字符为操作数,则
         readNumber ( S, opnd ); append ( RPN, opnd.top() ); //读入操作数,并将其接至RPN末尾
      } else //若当前字符为运算符,则
         switch ( priority ( optr.top(), *S ) ) { //视其与栈顶运算符之间优先级高低分别处理
            case '<': //栈顶运算符优先级更低时
               optr.push ( *S ); S++; //计算推迟,当前运算符进栈
               break;
            case '=': //优先级相等(当前运算符为右括号或者尾部哨兵'\0')时
               optr.pop(); S++; //脱括号并接收下一个字符
               break;
            case '>': { //栈顶运算符优先级更高时,可实施相应的计算,并将结果重新入栈
               char op = optr.pop(); append ( RPN, op ); //栈顶运算符出栈并续接至RPN末尾
               if ( '!' == op ) //若属于一元运算符
                  opnd.push ( calcu ( op, opnd.pop() ) ); //则取一个操作数,计算结果入栈
               else { //对于其它(二元)运算符
                  double opnd2 = opnd.pop(), opnd1 = opnd.pop(); //取出后、前操作数 /*DSA*/提问:可否省去两个临时变量?
                  opnd.push ( calcu ( opnd1, op, opnd2 ) ); //实施二元计算,结果入栈
               }
               break;
            }
            default : exit ( -1 ); //逢语法错误,不做处理直接退出
         }//switch
      /*DSA*/displayProgress ( expr, S, opnd, optr, RPN );
   }//while
   return opnd.pop(); //弹出并返回最后的计算结果
}

算法思想:
        ~~~~~~~        该算法自左向右扫描表达式,并对其中字符逐一做相应的处理。那些已经扫描过但(因信息不足)尚不能处理的操作数与运算符,将分别缓冲至栈opnd和栈optr。一旦判定已缓存的子表达式优先级足够高,便弹出相关的操作数和运算符,随即执行运算,并将结果压入栈opnd。
        ~~~~~~~        请留意这里区分操作数和运算符的技巧。一旦当前字符由非数字转为数字,则意味着开始进入一个对应于操作数的子串范围。由于这里允许操作数含有多个数位,甚至可能是小数。

void readNumber ( char*& p, Stack<double>& stk ) { //将起始于p的子串解析为数值,并存入操作数栈
   stk.push ( ( double ) ( *p - '0' ) ); //当前数位对应的数值进栈
   while ( isdigit ( * ( ++p ) ) ) //若有后续数字(多位整数),则
      stk.push ( stk.pop() * 10 + ( *p - '0' ) ); //追加之(可能上溢)
   if ( '.' == *p ) { //若还有小数部分
      double fraction = 1; //则
      while ( isdigit ( * ( ++p ) ) ) //逐位
         stk.push ( stk.pop() + ( *p - '0' ) * ( fraction /= 10 ) ); //加入(可能下溢)
   }
}

根据当前字符及其后续的若干字符,利用另一个栈解析出当前的操作数。解析完毕,当前字符将再次聚焦于一个非数字字符。

不同优先级的处置如下:
调用priority ()函数,将其与栈optr的栈顶操作符做一比较之后,即可视二者的优先级高低,分三种情况相应地处置。

Operator optr2rank ( char op ) { //由运算符转译出编号
   switch ( op ) {
      case '+' : return ADD; //加
      case '-' : return SUB; //减
      case '*' : return MUL; //乘
      case '/' : return DIV; //除
      case '^' : return POW; //乘方
      case '!' : return FAC; //阶乘
      case '(' : return L_P; //左括号
      case ')' : return R_P; //右括号
      case '\0': return EOE; //起始符与终止符
      default  : exit ( -1 ); //未知运算符
   }
}

char priority ( char op1, char op2 ) //比较两个运算符之间的优先级
{ return pri[optr2rank ( op1 ) ][optr2rank ( op2 ) ]; }

将其与栈optr的栈顶操作符做一比较之后,即可视二者的优先级高低,分三种情况相应地处置。

1)若当前运算符的优先级更高,则optr中的栈顶运算符尚不能执行
2)反之,一旦栈顶运算符的优先级更高,则可以立即弹出并执行对应的运算
	一目运算符!弹出一个数,双目运算符弹出两个数。
3)当前运算符与栈顶运算符的优先级“相等”
	对右括号的上述处理方式,将在optr栈顶出现操作符'('时终止。除左、
	右括号外,还有一种优先级相等的合法情况,即pri['\0']['\0'] = '='

5.3.2 逆波兰表达式(后缀表达式)

5.3.2.1 RPN

在这里插入图片描述
在这里插入图片描述

5.3.2.2 RPN实例

在这里插入图片描述

5.3.2.3 infix 到postfix 转换

在这里插入图片描述
在这里插入图片描述
上述evaluate()算法在对表达式求值的同时,也顺便完成了从常规表达式到RPN表达式的转换。

该算法借助append()函数将各操作数和运算符适时地追加至串rpn的末尾,直至得到完整的RPN表达式。

void append ( char* rpn, double opnd ) { //将操作数接至RPN末尾
   char buf[64];
   if ( ( int ) opnd < opnd ) sprintf ( buf, "%6.2f \0", opnd ); //浮点格式,或
   else                       sprintf ( buf, "%d \0", ( int ) opnd ); //整数格式
   strcat ( rpn, buf ); //RPN加长
}

void append ( char* rpn, char optr ) { //将运算符接至RPN末尾
   int n = strlen ( rpn ); //RPN当前长度(以'\0'结尾,长度n + 1)
   sprintf ( rpn + n, "%c \0", optr ); //接入指定的运算符
}

这里,在接入每一个新的操作数或操作符之前,都要调用realloc()函数以动态地扩充RPN表达式的容量,因此会在一定程度上影响时间效率。
在十分注重这方面性能的场合,读者可以做适当的改进——比如,有必要扩容时即令容量加倍。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1646320.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为ensp中USG6000V防火墙双机热备VRRP+HRP原理及配置

作者主页&#xff1a;点击&#xff01; ENSP专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年5月6日20点26分 华为防火墙双机热备是一种高可用性解决方案&#xff0c;可以将两台防火墙设备组成一个双机热备组&#xff0c;实现主备切换。当主用防火墙出现故障时&…

企业网站 | 被攻击时该怎么办?

前言 每天&#xff0c;数以千计的网站被黑客入侵。发生这种情况时&#xff0c;被入侵网站可用于从网络钓鱼页面到SEO垃圾邮件或者其它内容。如果您拥有一个小型网站&#xff0c;很容易相信黑客不会对它感兴趣。不幸的是&#xff0c;通常情况并非如此。 黑客入侵网站的动机与所…

书籍推荐|经典书籍ic书籍REUSE METHODOLOGY MANUALFOR等和verilog网站推荐(附下载)

大家好&#xff0c;今天是51过后的第一个工作日&#xff0c;想必大家都还没有完全从节假日的吃喝玩乐模式转变为勤勤恳恳的打工人模式&#xff0c;当然也包括我&#xff0c;因此这次更新主要是分享几篇书籍和verilog相关的学习网站~ 首先是一本数字电路相关的基础书籍&#xf…

深入理解Docker容器镜像

深入理解Docker容器镜像 1 容器是什么&#xff1a;特殊的进程 容器其实是一种沙盒技术。顾名思义&#xff0c;沙盒就是能够像一个集装箱一样&#xff0c;把你的应用“装”起来的技术。这样&#xff0c;应用与应用之间&#xff0c;就因为有了边界而不至于相互干扰&#xff1b;而…

流量分析。

流量分析 在Wireshak抓包可以看到正常的执行流程如下&#xff1a; ● Client向Server发起Load data local infile请求 ● Server返回需要读取的文件路径 ● Client读取文件内容并发送给Server ● PS&#xff1a;在本机上启动服务端与客户端&#xff0c;启动wireshark 抓包&…

navicat 连接 阿里云 RDS mysql 数据库

首先上官方教程连接 下面是我的实操记录 1、先输入正确的账号、密码 2、再加上数据库名称

省公派出国|社科类普通高校教师限期内赴英国访学交流

在国外访问学者申请中&#xff0c;人文社科类相对难度更大&#xff0c;尤其是英语语言学&#xff0c;作为非母语研究并不被国外高校看重。经过努力&#xff0c;最终我们帮助Z老师申请到英国坎特伯雷基督教会大学的访学职位&#xff0c;并在限期内出国。 Z老师背景&#xff1a; …

Chrome浏览器安装React工具

一、如果网络能访问Google商店&#xff0c;直接安装官方插件即可 二、网络不能访问Google商店&#xff0c;使用安装包进行安装 1、下载react工具包 链接&#xff1a;https://pan.baidu.com/s/1qAeqxSafOiNV4CG3FVVtTQ 提取码&#xff1a;vgwj 2、chrome浏览器安装react工具…

io (fscanf fprintf)

20 #include <sys/un.h>21 typedef struct stu22 {23 char name[16];24 int age;25 double score;26 }stu;27 int main(int argc, const char *argv[])28 {29 /* 有如下结构体30 31 申请该结构体数组&#xff0c;容量为5&#xff0c;初始化5个学生的信息32 …

C++:何为,。。。。。。

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

new mars3d.control.MapSplit({实现点击卷帘两侧添加不同图层弹出不同的popup

new mars3d.control.MapSplit({实现点击卷帘两侧添加不同图层弹出不同的popup效果&#xff1a; 左侧&#xff1a; 右侧&#xff1a; 说明&#xff1a;mars3d的3.7.12以上版本才支持该效果。 示例链接&#xff1a; 功能示例(Vue版) | Mars3D三维可视化平台 | 火星科技 相关代…

【6D位姿估计】ZebraPose 层次化分组策略 由粗到细的表面编码

前言 本文介绍6D位姿估计的方法ZebraPose&#xff0c;也可以称为六自由度物体姿态估计&#xff0c;输入单张图片&#xff0c;输出物体的三维位置和三维方向。 它来自CVPR2022的论文&#xff0c;通过层次化分组策略&#xff0c;高效地编码物体表面的信息。 ZebraPose提出了一…

基于Springboot的家具网站

基于SpringbootVue的家具网站设计与实现 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringbootMybatis工具&#xff1a;IDEA、Maven、Navicat 系统展示 用户登录 首页 商家 家具信息 家居资讯 后台管理 后台首页 用户管理 商家管理 家具类型管理 家具…

《Python编程从入门到实践》day20

#尝试在python3.11文件夹和pycharm中site-packages文件夹中安装&#xff0c;最终在scripts文件夹中新建py文件成功导入pygame运行程序 #今日知识点学习 import sysimport pygameclass AlienInvasion:"""管理游戏资源和行为的类"""def __init__(…

Python从0到100(二十):文件读写和文件操作

一、文件的打开和关闭 有了文件系统可以非常方便的通过文件来读写数据&#xff1b;在Python中要实现文件操作是非常简单的。我们可以使用Python内置的open函数来打开文件&#xff0c;在使用open函数时&#xff0c;我们可以通过函数的参数指定文件名、操作模式和字符编码等信息…

websevere服务器从零搭建到上线(四)|muduo网络库的基本原理和使用

文章目录 muduo源码编译安装muduo框架讲解muduo库编写服务器代码示例代码解析用户连接的创建和断开回调函数用户读写事件回调 使用vscode编译程序配置c_cpp_properties.json配置tasks.json配置launch.json编译 总结 muduo源码编译安装 muduo依赖Boost库&#xff0c;所以我们应…

智能BI产品设计

BI概念 目录 BI概念 一&#xff1a;与BI相关的几个重要概念 二&#xff1a;数据仓库 VS 数据库 BI架构 一&#xff1a;数据分析通用流程 二&#xff1a;BI平台基本架构 可视化图形 一&#xff1a;如何选择可视化图形 二&#xff1a;数据展示形式 三&#xff1a;数据…

【driver3】proc文件系统,内存分配,数据类型/移植/对齐,内核中断,通过IO内存访问外设,PCI

文章目录 1.创建proc文件系统接口&#xff1a;之前调试内核时都是通过prink打印内核信息&#xff0c;通过dmesg查看输出的信息。新调试方法&#xff1a;利用proc文件系统在pro文件夹下创建接口&#xff0c;读写这个接口就可实现对内核的调试2.内核内存分配函数&#xff1a;top&…

spring ioc 容器加载过程 refresh() 方法详解

IOC 加载过程 从 new ClassPathXmlApplicationContext开始 ApplicationContext context new ClassPathXmlApplicationContext("classpath:application.xml");ClassPathXmlApplicationContext类构造方法 public ClassPathXmlApplicationContext(String[] configLo…

自动驾驶融合定位:IMU内参模型及标定

自动驾驶融合定位&#xff1a;IMU内参模型及标定 一、 概述 标定的本质是参数辨识。首先明确哪些参数可辨识&#xff0c;其次弄清怎样辨识。 参数包括陀螺仪和加速度计各自的零偏、标度因数、安装误差。 辨识就比较丰富了&#xff0c;如果让各位先不局限于标定任务&#xf…