第四章:栈与队列

第四章:栈与队列

栈接口与实现

栈:只能在栈顶插入和删除的后进先出的数据结构。

由向量派生出的栈的实现:

#include "../Vector/Vector.h" //以向量为基类,派生出栈模板类
template <typename T> class Stack: public Vector<T> { //将向量的首/末端作为栈底/顶
public: //size()、empty()以及其它开放接口,均可直接沿用
   void push ( T const& e ) { insert ( size(), 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: //size()、empty()以及其它开放接口,均可直接沿用
   void push ( T const& e ) { insertAsLast ( e ); } //入栈:等效于将新元素作为列表的首元素插入
   T pop() { return remove ( last() ); } //出栈:等效于删除列表的首元素
   T& top() { return last()->data; } //取顶:直接返回列表的首元素
};

调用栈

实例:

消除递归:

递归函数的空间复杂度,主要取决于最大递归深度,而非递归实例总数。

消除递归可以将递归算法修改为迭代的形式。

比如:求阶乘的函数

尾递归:

在线性递归中,递归调用的语句是函数执行的最后一步称该递归为尾递归。

尾递归的消除:

栈的典型应用

进制转换

准确的说,这里我们仅讨论将十进制的数如何转化为其他进制的数。典型的方法是除基取余法,通过不断地除以基数,最后将所得的余数逆序输出即可。不难发现,该问题正是输出次序与处理过程颠倒,递归深度又不能提前预知,属于栈的逆序输出的应用。

实现:

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中

这里由于超过十进制的数可能带字母,故先用digit数组存放余数应该输出的相视,根据求得的余数映射到digit数组中再压入栈中。

括号匹配

括号匹配属于递归嵌套问题,具有自相似性的问题可递归描述,但分支位置和嵌套深度不固定。

上面给出了减治和分治失败的尝试。

事实上,虽然由表达式(E)匹配,我们得不出E匹配,但是L()R匹配,当且仅当LR匹配,也就是说,消除相邻的一对括号不会影响整体的匹配判断。

于是,扫描表达式,遇左括号入栈,遇右括号出栈,出栈时,若栈非空,则说明可以消除一对紧邻的括号,若栈空,说明当前右括号前没有与之匹配的左括号。当整个表达式扫描完成,若表达式的括号是匹配的,则栈应该为空,否则不匹配。

如果仅有一种括号,则使用一个计数器足以,遇见左括号计数器+1,遇见右括号计数器-1,只要扫描过程中计数器非负,并且扫描完成计数器归零即表明表达式匹配。

如果是多种括号嵌套,则不能使用计数器。

栈混洗

栈混洗,即将栈A中元素压入栈S再弹出到B能得到一个序列,称为A的栈混洗序列。

比如A从栈顶到栈底依次为1 2 3,经过栈混洗能得到的序列为1 2 3(IOIOIO),1 3 2(IOIIOO),2 1 3(IIOOIO),2 3 1(IIOIOO),3 2 1(IIIOOO),但是无法得到3 1 2.

设SP(n)表示长度为n的序列可能的栈混洗的总数:

易知SP(1)= 1.设A序列为1,2,3,…,n。则经过栈混洗后得到的序列为同样的一个长度为n的序列。元素1是第一个被压入S中的,如果它第一个被弹出,则处在新序列的第一个位置,第二个被弹出,则处在第二个位置,设1在第k次pop后被弹出,此时,栈S首次变空,

此时B一共有k – 1个元素,加上最后弹出的1固定在末尾。而A中还剩n-k个元素,故栈混洗序列中1处在第k个位置的情况一共有SP(k-1)*SP(n-k)种,k取1到n。

最后求得的栈混洗数目SP(n) = Catalan(n)。

全排列一共n!种,但是合法的栈混洗只是其中一部分,不合法的序列经过证明一定含有“312”禁形,不存在3 1 2禁形的一定是栈混洗序列。

另外的几种说法:

1 <= i < j < k <= n,则…k … i .. j序列一定不是栈混洗。

i < j,则…j + 1…i…j序列也不是栈混洗。

判断一个序列是否是栈混洗序列,则只需借用三个栈对栈混洗的过程进行模拟即可,时间复杂度为O(n)。

栈混洗与括号匹配

中缀表达式求值

中缀表达式求值即给定一个包含四则运算,乘方、阶乘以及括号的运算表达式,通过处理求得表达式的值。属于栈的延迟缓冲的应用。

大致的思路很简单:就是维护一个操作符栈和一个操作数栈,从左到右扫描表达式,扫到操作数就压入操作数栈中,扫到操作符和与操作符栈顶元素(若存在)比较优先级,栈顶运算符优先级较低则将当前运算符压入栈中,继续扫描,栈顶运算符优先级较高则出栈并将参与运算的运算数也出栈,计算后将运算结果压入栈中,同时继续比较当前运算符与栈顶运算符的优先级。

然而算法的具体实现需要考虑还有很多。比如操作数不止一位时怎么处理?遇见括号优先级如何判定?前一个问题很简单,可以用一个变量存储读入的操作数,比如读取12*3,s初值设为0,读到1,s = s*10 + 1 = 1,读到2,s = s * 10 + 2 = 12,然后读到*号时,将s压入操作数栈,同时s重新置为0即可。

后一个问题与其说是括号优先级如何判别,倒不如说所有运算符间优先级该如何判别,优先级表如下:

表的列为栈顶运算符,行为当前运算符,具体优先级规则如下:

在不考虑括号和结束\0时,优先级顺序为:

1.单目运算符优先级最高,也就是说阶乘运算大于加减乘除和乘方运算。

2.双目运算符中,乘方>乘除>加减

3.相同级别运算符先入栈的优先级更高,比如栈顶元素是+,扫到的当前运算符也是+,则应该先将栈顶元素退出运算。这里加减运算、乘除运算理论上同一个级别,在这里先入栈的优先级更高。

对于括号运算符:

  1. 对于左括号,在栈顶时优先级低于所有算术运算符,不在栈顶时运算符高于所有运算符。

既然相同的运算符都随着入栈先后顺序的不同导致了优先级的不同,那么什么情况下优先级是相同的呢?

2.左括号的优先级等于右括号的优先级,。

前面在说算法思路时说过,栈顶优先级高则出栈参与运算,栈顶优先级低则将当前运算符入栈,那么优先级相等时呢?出现优先级相等一般只会是右括号和\0.对于右括号,不论栈顶的运算符是什么,只要不是左括号,都视为优先级比右括号高,所以不停的出栈参与运算,直至最后遇见左括号,左括号出栈。所以优先级相等时执行的操作是将与之优先级相等的运算符出栈。

对于\0:

由于\0是表达式字符的结束标记,为了统一,我们在计算前先给操作符栈压入一个哨兵\0,\0在栈顶时优先级低于所有运算符,直至遇见了和它优先级相等的末尾的\0,出栈之际操作符栈为空,操作数栈唯一的元素即是运算结果。

算法:

逆波兰表达式

逆波兰表达式又称后缀表达式(RPN),是912试卷上的常客。

我们在手工计算中缀表达式时,快速判断下优先级,然后算出结果,但是对于计算机而言,计算中缀表达式的值就要执行之前所概括的那么复杂的算法。而RPN则避免了优先级的约数,在RPN中,遇见操作数就可以入栈,遇见操作符就退出操作数运算,而且没有括号参与。

既然后缀表达式计算如此快捷,那么如何将中缀表达式转化为RPN呢?

手工转换的过程如上图,在每次运算上都加上括号,再将运算符移到对应括号右侧,最后删掉括号即可。同时可以看见RPN一个显著的特点,转换为RPN,运算符的顺序可能改变,运算数的顺序并不会发生改变。

中缀转后缀的算法是在中缀表达式求值算法中略加修改就可以得到的。即扫到操作数就接到RPN后面,扫到运算符,当它到了执行的时刻就接入RPN。因此RPN保持了操作数的顺序。

既然evalute算法已经能够求值,那么完成RPN转换有何意义?

同样长度(指同样多操作数)的 RPN 比中缀表达式算得快。

课件上的转换例子,每个操作数都是一个具体的数,把中缀表达式转成 RPN 的过程已经足以把中缀表达式计算出来了。正如你所理解的,这种情况下转成 RPN 再计算得不偿失。

但是,操作数可能是一个未知数。例如中缀表达式 (a + b) * c 分别代入 n 组具体数字计算表达式值,例如代入 (a=1,b=2,c=3)  (a=4,b=5,c=6)  (a=10,b=11,c=12) 求值。不转就要算 n 次中缀表达式,转就只要转 1 + n RPN

相对于原表达式,RPN不一定所需空间更少,因为作为补偿,往往还要引入作为分隔作用的元字符(比如空格)。

队列接口与实现

队列-支持在队尾插入和队尾删除的先入先出的线性序列。

模板类:

#include "../List/List.h" //以List为基类
template <typename T> class Queue: public List<T> { //队列模板类(继承List原有接口)
public: //size()、empty()以及其它开放接口均可直接沿用
   void enqueue ( T const& e ) { insertAsLast ( e ); } //入队:尾部插入
   T dequeue() { return remove ( first() ); } //出队:首部删除
   T& front() { return first()->data; } //队首
};

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值