目录
[1.1 栈的基本概念](#1.1 栈的基本概念)
[1.1.1 栈的抽象数据类型](#1.1.1 栈的抽象数据类型)
[1.1.2 顺序栈](#1.1.2 顺序栈)
[1.1.3 链式栈](#1.1.3 链式栈)
[1.2 栈与递归](#1.2 栈与递归)
[1.2.1 递归的定义](#1.2.1 递归的定义)
[1.2.2 递归函数的实现](#1.2.2 递归函数的实现)
[1.2.3 递归算法到非递归的转换](#1.2.3 递归算法到非递归的转换)
[1.3 栈的深入讨论](#1.3 栈的深入讨论)
[1.3.1 顺序栈与链式栈的比较](#1.3.1 顺序栈与链式栈的比较)
[1.3.2 限制存取点的表](#1.3.2 限制存取点的表)
栈(stack)和队列(queue)作为两种重要的线性结构,在计算机科学中具有非常广泛的应用,从简单的表达式计算到编译器对程序语法的检查,再到操作系统对各种设备的管理等都会涉及。
从逻辑结构来说,栈和队列都是典型的线性结构。与线性表不同的是,栈和队列上的操作比较特殊,受到一定的限制,仅允许在线性表的一端或两端进行。因此,栈和队列常被称为操作受限的线性表,或者限制存取点的线性表。
栈和队列在日常生活中均有许多实例,例如叠放在一起的一摞盘子可看成栈的一个实例,而在银行排队取款的一行人则构成一个典型的队列。一般而言,栈的特点是后进先出,常用来处理具有递归结构的数据;而队列的特点则是先进先出,在实际中体现出公平的原则,可以用来暂时存放需要按照一定次序依次处理但尚未处理的元素。
本童将对栈这种结构及其典型应用进行介绍。
1.1 栈的基本概念
栈是一种限定仅在一端进行插入和删除的线性表,无论是往栈中插人元素还是删除栈中的元素,或者读取栈中的元素,都只能固定在线性表的一端进行。通常,栈的这一端被称为栈顶(top) ,与此相对,栈的另一端叫做栈底(bottom)。由此可知,最后插人栈中的元素是最先被删除进栈或读取的元素,而最先压人的元素则被放在栈的底部,要到最后才能取出。换言之,栈的修改是按后进先出的原则进行。因此,通常栈被称为后进先出(lastinfirstout)表,简称LIFO表,如图3.1所示。例如,碗橱里的一叠盘子、大型火车站用于调整调度火车头方向的调度栈结构等都可视为栈的模型。

尽管操作受限降低了栈的灵活性,但也正因为如此而使得栈更有效且更容易实现。栈的应用非常广泛,并因此形成了栈的一些特殊术语。习惯上称往栈中插人元素为push 操作,简称为压栈或人栈 ;删除栈顶元素被称为pop 操作,简称为出栈或弹出 。
如同线性表可以为空表一样,没有元素的栈称为空栈。例如,刚建立的栈一般是空栈,随着栈中所有元素的删除,栈也会变成空栈。
1.1.1 栈的抽象数据类型
基于栈的特性,定义在栈的抽象数据类型中的运算包括进栈push、出栈pop、读栈顶top等常用操作以及判断栈是否为空的isEmpty和栈是否已满的isFu等边界判断操作。根据抽象和封装的原则,只可通过其抽象数据类型定义的运算来对栈进行操作。
下面的C++类模板给出了栈类(名字为 Stack,模板参数为元素类型T)的一个抽象数据类型定义。
【代码1.1】栈的抽象数据类型定义
cpp
template <class T> //栈的元素类型为T
class Stack {
public: //栈的运算集
void clear( ); //变为空栈
bool push(const T item); //item入栈,成功则返回真,否则返回假
bool pop(T & item); //返回栈顶内容并弹出,成功返回真,否则返回假
bool top(T & item); //返回栈顶内容但不弹出,成功返回真,否则返回假
bool isEmpty(); //若栈已空返回真
bool isFull(); //若栈已满返回真
};
上述定义的这个抽象数据类型并不是唯一的。针对具体应用的不同要求,操作函数可以适当增删。例如,栈的链表实现中并不需要判断栈是否为满,某些实现中也许会把读取栈顶元素的top操作和出栈操作pop合二为一。
栈的实现与其存储结构相关,下面介绍顺序栈和链式栈两种存储结构及相应的栈操作.
1.1.2 顺序栈
采用顺序存储结构的栈称为顺序栈(array-basedstack),需要一块连续的区域来存储栈中的元素,因此需要事先知道或估算栈的大小。
顺序栈本质上是简化的顺序表。对元素数目为n的栈,首先需要确定数组的哪一端表示栈顶。如果把数组的第0个位置作为栈顶,按照栈的定义,所有的插人和删除操作都在第0个位置上进行,即意味着每次的push或pop操作都需要把当前栈的所有元素在数组中后移或前移一个位置,时间代价为0(n)。反之,如果把最后一个元素的位置-1作为栈顶,那么只需将新元素添加在表尾,出栈操作也只需删除表尾元素,每次操作的时间代价仅为0(1)。图3.2所示为按后一种方案实现的栈,其中t表示栈顶。

顺序栈实现时,用一个整型变量top(通常称为栈顶指针)来指示当前栈顶位置,同时也可以表示当前栈中元素的个数。代码1.2给出了一个顺序栈类及其部分成员函数的实现方法。
【代码1.2】栈的顺序实现
cpp
template <class T>
class arrStack: public Stack <T> {
private: //栈的顺序存储
int mSize; //栈中最多可存放的元素个数
int top; //栈顶位置,应小于mSize
T *st //存放栈元素的数组
public: //栈的运算的顺序实现
arrStack(int size) { //创建一个给定长度的顺序栈实例
mSize = size;
top = -l;
st = new T[mSize];
}
arrStack() { //创建一个顺序栈的实例
top = -l;
}
~arrStack() { //析构函数
delete [] st;
}
void clear() { //清空栈内容
top = -1;
}
bool push (const T item) { //入栈操作的顺序实现
if(top == mSize-1) { //栈已满
cout<< "栈满溢出" << endl;
return false;
}
else { //新元素人栈并修改栈顶指针
st[++top] = item;
return true;
}
}
bool pop(T & item) { //出栈的顺序实现
if(top == -1) { //栈为空
cout << "栈为空,不能执行出栈操作" << endl;
return false;
}
else {
item = st[top--]; //返回栈顶元素并修改栈顶指针
return true;
}
}
bool top(T & item) { //返回栈顶内容,但不弹出
if(top == -1) { //栈空
cout << "栈为空,不能读取栈顶元素" << endl;
return false;
}
else {
item = st[top];
return true;
}
}
}
top可以有两种定义方式。一种是将其设置为栈中第一个空闲位置 ,即空栈的top为0 ;另一种则把 top 定义为栈中最上面的那个元素的位置 ,而非第一个空闲位置,此时空栈的 top 应该初始化为-1 或任何非自然数。在本书中采取后者 来表示栈顶。因为前者会浪费数组的一个位置。这样,push 和 pop 操作只在 top 所指示的数组位置插人或删除一个元素。因为 top 表示栈中最上面元素的位置,所以 push 一个元素时首先把top 加1,然后把新元素插入到新的栈顶位置。同样,pop先删除栈顶元素,再把top 减1。
图3.3所示为mSize取值为6的顺序栈s中数据元素和栈顶指针的变化。其中,图3.3(a)表示空栈状态,此时stop=-1;图3.3(b)表示栈中具有一个元素的情况,此时栈顶top 所指为栈顶元素所在的位置0;图3.3(c)则表示在进行若干次进栈操作之后栈满的情况,此时 top=mSize-1:图3.3(d)则是在图3.3(c)基础上连续出栈两次后的状态。
栈中元素是动态变化的,当栈中已有mSize个元素时,进栈操作会产生上溢出(overflow)。相应的,在空栈上进行出栈操作则会造成下溢出(underflow)。为了避免溢出,在对栈进行 push和 pop 操作之前需要检査栈是否已满或是否已空。

【算法1.3】改进的进栈操作
cpp
template <class T>
bool arrStack<T> :: push(const T item) {
if(top == mSize -1) {
T *newSt = newT[mSize*2];
for(i = 0; i <= top; i++)
newSt[i] = st[i};
delete [] st; //释放原栈
st = newSt;
mSize *= 2;
}
st[++top] = item;
return true;
}
如果出现上溢时仍希望对顺序栈执行进栈操作,可以考虑对当前的顺序栈进行适当的扩容。例如,像算法1.3那样,申请一个扩大一倍的新数组,把顺序栈的原有内容顺序移动到新的数组中,再按正常的方式来执行进栈操作。
1.1.3 链式栈
链式栈(linked stack)本质上是简化的链表。需要注意的是为了方便存取,栈顶元素应该设置为链表头。图3.4所示为链式栈的一个简单示意图。
代码1.4是链式栈的一个简单实现。其中,数据成员top是一个指向链式栈的首结点(栈顶)的指针,链表的结点类型采用第2章中已定义过的Link类模板。进栈操作push 在链表头插入元素,出栈操作pop删除链头元素并释放空间。显而易见,push和pop的时间代价均为O(1)。

【代码1.4】栈的链式实现
cpp
template <ciass T>
class lnkStack : public Stack <T> {
private: //栈的链式存储
Link<T>* top; //指向栈顶的指针
int size; //存放元素的个数
public: //栈运算的链式实现
lnkStack(int defSize) { //构造函数
top = NULL;
size = 0;
}
~lnkStack() { //析构函数
clear();
}
void clear() { //清空栈内容
while(top!=NULL) {
Link<T> * tmp = top;
top = top -> next;
delete tmp;
}
size = 0;
}
bool push(const T item) { //入栈操作的链式实现
Link<T> * tmp = new Link<T>(item,top);
top = tmp ;
size++;
return true;
}
bool pop(T& item) { //出栈的链式实现
Link<T> * tmp;
if(size == 0) {
cout << "栈为空,不能执行出栈操作" < <endl;
return false;
}
item = top -> data;
tmp = top->next;
delete top;
top = tmp;
size--;
return true;
}
bool top(T & item) { //返回栈顶内容,但不弹出
if(size == 0) {
cout << "栈为空,不能读取栈顶元素" << endl;
return false;
}
item = top -> data;
return true;
}
}
1.2 栈与递归
由于符合人类自顶向下抽象描述问题的思维方式,递归成为数学和计算机科学的基本概念是解决复杂问题的一个有力手段。许多程序设计语言都支持递归,这些支持本质上是通过栈来实现的。
本节将以阶乘函数的计算为例,分析函数的递归调用在程序运行阶段的工作过程。在此基础上,以背包问题为例说明递归算法到非递归的一种机械转换方法。
1.2.1 递归的定义
以阶乘函数为例来说明递归的定义。阶乘n!的递归定义如下:

为了定义整数n的阶乘,必须先定义(n-1)的阶乘,而为了定义(n-1)的阶乘,又需先定义(n-2)的阶乘,如此直到0为止,因为此时阶乘定义为1。这种用自身的简单情况来直接或间接地定义自己的方式称为递归定义。
可以看出,一个递归定义由两部分组成。其一为递归基础,也称递归出口,是递归定义的最基本情况,也是保证递归结束的前提;其二则为递归规则,确定了由简单情况构筑复杂情况需遵循的规则。上面的阶乘定义中递归出口即为n≤0,此时阶乘定义为1;递归规则为nx(n-1)!即n的阶乘由(n-1)的阶乘来构筑。这个递归定义可在C++语言中由下面的这个递归函数来实现:
【算法1.6】阶乘函数
cpp
long factorial(long n) {
if(n <= 0)
return 1;
return n *factorial(n-l);
}
1.2.2 递归函数的实现
大多数程序设计语言运行环境所提供的函数调用机制是由底层的编译栈支持的。编译栈中的"运行时环境"指的是目标计算机上用来管理存储器并保存执行过程所需信息的寄存器及存储器的结构。
在非递归调用的情况下,数据区的分配可以在程序运行前进行,直到整个程序运行结束再释放,这种分配称为静态分配。采用静态分配时,函数的调用和返回处理比较简单,不需要每次分配和释放被调用函数的数据区。在递归调用的情况下,被调函数的局部变量不能静态地分配某些固定单元,而必须每调用一次就分配一份,以存放当前所使用的数据,当返回时随即释放。这种只有在执行调用时才能进行的存储分配称为"动态分配",此时需要在内存中开辟一个称为运行栈(runtime stack) 的足够大的动态区。
用作动态数据分配的存储区可按多种方式组织。典型的组织如图3.6所示,将存储器分为栈(stack)区域和堆(heap)区域,栈区域用于分配具有后进先出LIFO特征中的数据(如函数的调用),而堆区域则用于不符合LIFO的数据(如指针的分配)的动态分配。
运行栈中元素的类型(即被调函数需要的数据区类型)涉及动态存储分配中的一个重要念:函数活动记录(activationrecord)。当调用或激活一个函数时,相应的活动记录包含了为该函数的局部数据所分配的存储空间。通常,活动记录至少应包括如图3.7所示的几部分内容。

每次调用一个函数时,执行进栈操作,把被调函数所对应的活动记录分配在栈的顶部;而在每次从函数返回时,执行出栈操作,释放本次的活动记录,恢复到上次调用所分配的数据区中。因此,被调函数中变量地址全部采用相对于栈顶的相对地址来表示。因为运行栈中存放的是被调函数的活动记录,所以运行栈又称为活动记录栈(stack of activation record) 。同时,由于运行栈按照函数的调用序列来组织,因此也称为调用栈(callstack) 。
一个函数在运行栈中可以有若干不同的活动记录,每个代表了一个不同的调用。对于递归函数来说,递归深度就决定了其在运行栈中活动记录的数目。当函数进行递归调用时,函数体的同一个局部变量在不同的递归层次被分配给不同的存储空间,放在运行栈的不同位置。
概括来讲,函数调用可以分解成以下3步来实现:
(1)调用函数发送调用信息,包括调用方要传送给被调方的信息,如传给形式参数(简称形参)的实在参数(简称实参)的值、函数返回地址等。
(2)分配被调方需要的局部数据区,用来存放被调方定义的局部变量、形参变量(存放实参的值)返回地址等,并接收调用方传送来的调用信息。
(3)调用方暂停,把计算控制转移到被调方,即自动转移到被调函数的程序入口。
当被调方结束运行,返回到调用方时,其返回处理一般也分解为3步进行:
(1)传送返回信息,包括被调方要传回给调用方的信息,诸如计算结果等。
(2)释放分配给被调方的数据区。
(3)按返回地址把控制转回调用方。
设要计算4的阶乘,在C/C++语言中可以设计一个主程序main(参考算法 1.7)来调用算法1.6定义的阶乘函数。
【算法1.7】计算阶乘的主程序
cpp
#include <iostream>
void main() {
long x;
cin >> x;
cout << factorial(4)<< endl:
}
主程序通过 factorial(4)这个语句向阶乘函数facrotial()的形参n提供了实参4。通过调用建立阶乘函数factorial()的一个活动记录,把当前的必要信息,包括返回地址、参数(此时传人4)、局部变量等存人栈中,如图3.8(a)所示。在计算 factorial(4)时,调用了factorial(3),此时需要为新的被调函数建立活动记录(此时传人的参数为3)并压人栈中,成为新的栈顶,依次类推factorial(3)调用又引起 factorial(2)的调用,栈顶再次更新,依次调用直到最终调用 factorial(0)此时 factorial(0)的活动记录成为新的栈顶。由于factorial(0)满足递归的出口条件,可以直接得到结果,执行结束后,其活动记录从栈顶弹出,并将计算结果和控制权返回给其调用方factoria(1)。factorial(1)根据factorial(0)的返回结果1可以计算出1!=1,执行结束后,也从栈顶弹出其活动记录,继续将控制权转移给它的调用方factorial(2),如图3.8(b)所示,按进栈顺序的反序依次从栈中删除每个活动记录,把计算结果和控制权逐层上移,最后factorial(4)把控制连同计算结果 24返回给调用它的main()函数。这样,当在main()中执行cout语句时,只在运行时环境中保留了main()和全局/静态区域的活动记录。
1.2.3 递归算法到非递归的转换
递归的算法具有可读性强、结构简练、正确性易证明等优点,但是在时空的开销上相对较大。为提高算法的时空效率,尤其是在某些对响应时间很敏感的实时应用环境下,或在不支持递归的程序环境中,必须将递归算法转化为非递归算法,问题才能得到有效解决。
把一个递归算法转化为相应的非递归算法的方法很多。本节重点介绍一种利用栈进行转换的方法,以进一步揭示递归的本质以及栈与递归的内在联系 。

递归有很多分类方法。例如,根据递归所处的位置可分为尾部递归和非尾部递归,所谓尾部递归是指递归函数中最后一个操作是一个递归调用,之后不再有其他语句;与尾部递归相对的是非尾部递归。此外,根据递归的调用方式可分为直接递归和间接递归,根据有无嵌套还可分为嵌套递归和无嵌套递归等。
根据是否需要回溯,还可以把递归分成简单递归和复杂递归两种。简单递归一般可以根据递归式来找出其递推公式。例如,阶乘函数这样简单的递归基本上可以用循环迭代的方式来取代递归,循环结束条件通常比较容易确定。阶乘的选代实现如算法1.8所示。
【算法1.8】阶乘的迭代实现
cpp
long factorial(long n) {
long m = 1;
long i;
if(n > 0)
for(i = l; i <= n; ++)
m = m * i;
return m;
}
而复杂递归的循环结束条件就不那么容易确定,需要对整个递归程序进行分析。一般情况下,可模拟编译系统处理递归的机制,使用栈等数据结构保存回点来求解。
以阶乘函数为例,其递归出口为n<=0时返回结果1,在n>0的情况下均需按照一个递归规则来统一处理:求其前驱n-1的阶乘。因此,在利用栈将其转换成非递归的过程中,遇到不等于0的参数n,则按递归规则将其压栈,并将其值减1;当遇到0时则停止递归,将其结果返回给上层调用函数。算法3.9是根据此转换过程得到的非递归算法的模拟实现,其中,由于递归出口的返回值为1,因此将中间变量m的初值置为1,以简化出口的处理。
【算法1.9】阶乘的一种非递归实现
cpp
long factorial(long n) {
Stack <long> s;
long tmp;
long m = l;
while(n > 0) //不满足递归出口
s.push(n--); //按递归规则把相应数据压栈
while(s.pop(&tmp)) //满足递归出口,开始进行返回处理
m *= tmp;
return m;
}
通过上面介绍的递归函数的实现机制,可以看出系统处理递归的简单准则:当遇到递归规则(涉及递归调用)时进行压栈操作,把递归函数的相关信息保存在栈中;当遇到递归出口时则进行出栈的操作,把结束了的被调递归函数的结果等信息返回上一级的调用函数。
下面介绍模拟编译系统的递归机制,机械地把一个包含t个递归规则的复杂递归算法转换为非递归算法的过程。首先设置一个工作栈,每遇到一个递归规则都进行压栈操作,遇到递归出口则进行出栈操作,并根据其对应的递归规则来进行相应的递归返回处理。共有t种不同的返回处理情况,外加整个递归程序的入口和出口处理,总共t+2种不同的处理情况。具体的模拟过程可分成以下几个步骤:
【算法1.10】背包问题的递归算法
cpp
bool knap(int s,int n) {
if(s == 0)
return true;
if((s < 0) || (s > 0 && n < 1))
return false;
if(knap(s - w[n-1], n - 1)) {
cout << w[n-1]
return true;
}
else return knap(s, n-1);
}
可以看到,算法 1.10中递归的出口有两个:其一,当背包的承重量为0时,背包问题有解,只要不选择任何物品即可;其二,当背包承重量为负,或者虽然背包承重量大于0但物品个数小于1时,背包问题无解。当满足这两个出口之一的条件时,背包问题便结束递归。尚未达到递归出口时,背包问题的递归规则也有两种:
- 规则1:若w[n-1]包含在解中,求解 knap(s-w[n-1],n-1)。
- 规则2:若w[n-1]不包含在解中,求解knap(s,n-1)。
首先从设计栈开始,栈中每个结点应该包含以下4个域:参数s和n,返回地址rd和结果单元k。由于knap算法中有两处递归规则,因此加上递归出口返回地址共有3种情况:
- (1)计算knap(s,n)完毕返回到调用本函数的其他函数。
- (2)计算knap(s-w[n-1],n-1)完毕,返回到本调用函数继续计算
- (3)计算knap(s,n-1)完毕,返回到本调用函数继续计算。
为区分这3种返回情况,rd分别用0、1、2表示。因此,栈结点可声明如下:
cpp
enum rdType {0, 1, 2};
public class knapNode {
int s, n; //背包的承重量和物品的数目
rdType rd; //返回地址
bool k; //结果单元
}
并引人两个与栈中结点类型相同的变量tmp和x作为进出栈的缓冲
cpp
knapNode tmp, x;
定义一个栈变量:
cpp
Stack<knapNode> stack;
【算法1.11】背包问题的非递归实现
cpp
bool nonRecKnap(int s, int n) {
tmp.s = s, tmp.n = n, tmp.rd = 0; //非递归调用入口
stack.push(tmp);
label0: //递归调用人口
stack.pop(&tmp); //查看栈顶元素并分情况处理
if(tmp.s == 0) { //若满足递归出口条件
tmp.k = true; //修改栈顶的结果单元k
stack.push(tmp);
goto label3; //转向递归出口处理
}
if(tmp.s < 0) || (tmp.s > 0 && tmp.n < 1) {
tmp.k = false; //修改栈顶的结果单元k
stack.push(tmp);
goto label3;
}
stack.push( tmp); //尚未满足递归出口
x.s=tmp.s - w[tmp.n-1]; //按照规则1进行压栈处理
x.n=tmp.n - l;
x.rd = 1;
stack.push(x);
goto label0;
label1: //规则1对应的返回处理
stack.pop(&x); //查看栈顶,根据其内容分情况处理
if(tmp.k == true) { //若某层的结果单元为true
x.k = true; //把true结果上传给调用方
stack.push(x);
cout << w[x.n-1] << endl; //并输出对应的物品
goto label3;
}
stack.push(x); //若某层的结果单元为false
tmp.s = x.s; //当前物品的选择不合适,回溯,调用规则2
tmp.n = x.n - 1; //按照规则2进行压栈处理
tmp.rd = 2;
stack.push(tmp);
goto label0;
label2: //规则2对应的返回处理
stack.pop(&x);
x.k= tmp.k; //结果单元k的内容上传给调用方
stack.push(x);
label3: //递归出口处理
stack.pop(&tmp);
switch(tmp.rd) {
case 0: return tmp.k; //算法结束并返回结果
case l: goto labell; //转向规则1的返回处理处
case 2: goto label2; //转向规则2的返回处理处
}
}
而后,转换按下面方式进行:

这样转换之后便可得到一个如算法1.11所示的功能相同的非递归算法。该算法执行结束时,背包问题的解在tmp.k中,若其值为tue,则具体选中的 w[i]已在算法中输出。
可以看出,算法1.11中有多处弹出栈顶元素、修改其中的结果单元k后又重新压栈的操作。仔细分析这些操作之后,会发现算法1.11还有进一步优化的余地。首先,从计算过程来看,一旦在某层调用中相应栈顶元素的k值为true,则此背包问题的解为真,并且将trnue 逐层向外传递,将各层的k都置成true,因此可采用一个全程变量k作为结果单元,将其初始状态设置为false。一旦置成true后就不再变化,从而可省去栈中各结点的k数据域及其多次的重复赋值和进栈、出栈操作。其次,从调用过程来看,每递归进人一层,参数n就减1,栈的指针 top 就加1,只要在计算前将 top 置为 -1,则递归过程总有栈顶元素的n数据域满足 st[top].n+t=no,st[top].n<l的条件就等价于t>n。-1,如果栈顶指针可访问,就可省去栈中的数据域n,用t>n。-1替换算法中tmp.n<1:最后,消除算法中的goto语句,便得到更为简洁优化的算法1.12。改进后的版本 中栈的结点类型替换成下述的形式:
cpp
public class knapNode {
int s; //背包的承重量
rdType rd; //返回地址
}
【算法1.12】背包问题的非递归实现的优化版
cpp
bool nonRecKnapOpt(int s, int n) {
int t,n0 = n;
bool k = false;
tmp.s = s, tmp.rd = 0;
stack.push(tmp);
while(!stack.isEmpty()) {
t = stack.top;
stack.top(&tmp);
while(tmp.s >= 0) && (tmp.s <= 0 || n0 > t) { //处理栈顶元素,以判断是否满足
//递归出口条件
if(tmp.s == 0) {
k = true;
break;
} else { //尚未达递归出口前,按规则1进行压栈操作
x.s = tmp.s - w[n0 - 1 - t];
x.rd = 1;
stack,push(x);
}
t = stack.top;
stack.top(&tmp);
}
while(!stack.isEmpty()) { //返回处理
stack.pop(&tmp);
t = stack.top;
if(tmp.rd == 0) //算法结束
return k;
if(tmp.rd == 1) //从规则1返回
if(k == true) //结果为真则打印对应的物品
cout << w[n0 - 1 - t] <<endl;
else { //否则回溯,采用规则2进栈
stack.top(&x);
tmp.s = x.s;
tmp.rd = 2;
stack.push(tmp);
break;
}
}
}
}
1.3 栈的深入讨论
1.3.1 顺序栈与链式栈的比较
栈的应用非常广泛,只要满足后进先出的特性都可以使用栈结构。例如,函数调用和递归实现、深度优先周游、表达式转换和求值等。
1. 顺序栈与链式栈的比较
顺序栈和链式栈的基本操作都只需要常数时间,因此二者在时间效率上难分伯仲。从空间角度来看,初始时顺序栈必须说明一个固定的长度,当栈不够满时,势必浪费一些空间;链式栈的长度可根据需要而增减,但每个元素都需要一个指针域,从而产生结构性开销。
在栈的实际应用中,有时需要访问栈的内部元素。顺序栈可以根据元素与栈顶的相对位置快速定位并读取内部元素,而链式栈则需要沿着指针链遍历才能访问内部元素。
鉴于上述原因,顺序栈在实际中更为常用一些。
2. 多栈管理
在编译系统等计算机系统软件中,往往同时使用和管理多个栈,这时可充分利用顺序栈单向延伸的特性。例如,可以使用一个数组来存储两个栈,让它们共享同一存储空间。把数组的两端设置为两者各自的栈底,从两端开始向中间迎面延伸(见图3.13),这样浪费的空间相对少一些在两个栈的空间需求恰好相反,即一个栈增长另一个收缩时,这种方法很奏效。反之,如果两个栈同时增长,数组中间的空间会很快用完。

1.3.2 限制存取点的表
利用栈和队列的思想可以设计出一些变种的栈或队列结构。虽然它们的应用没有栈和队列那样广泛,但在一些特定情况下具有很好的应用价值。
(1)双端队列:限制插人和删除在线性表的两端进行。栈和队列都是特殊的双端队列,对其存取设置了限制。
(2)双栈:两个底部相连的栈。双栈是一种添加限制的双端队列,它规定从end1 插入的元素只能从end1端删除,而从end2 插人的元素只能从 end2 端删除。
(3)超队列:一种删除受限的双端队列,删除只允许在一端进行,而插入可在两端进行。
(4)超栈:一种插人受限的双端队列,插人只限制在一端而删除允许在两端进行。
由此可见,这几种限制存取点的表都是某种受限的双端队列。STL提供的基本序列中就有双端队列 deque,栈和队列都是通过 deque 实现的。
本章小结
栈是一种限制访问端口的线性表,常被称为后进先出(LIFO)表。其元素的插入和删除都只能在表的一端进行,该端称为栈的栈顶,另一端则叫做栈底。栈的特点是每次取出(并被删除)的元素总是当前栈内元素中最后压人的,而最先压人的元素则被放在栈的底部,要到最后才能取出。
栈的运算包括压栈、出栈、返回栈顶元素、判断栈是否为空等运算,运算的时间代价均为常数时间。
栈的实现通常有顺序栈和链式栈两种。顺序栈用数组实现;而链式栈则用单链表方式存储其中的指针方向是从栈顶向下链接。在实际中,顺序栈比链式栈使用得更为广泛。
栈是一种很重要、应用非常广泛的数据结构。常见的应用包括表达式转换和求值、函数调用和递归实现、深度优先搜索等。栈的一个很重要的应用在于对函数调用机制和递归实现的支持本章通过递归函数的实现机制和递归算法到非递归算法的转换揭示了递归的本质以及栈与递归的内在联系。
本博文目的在于总结整理并介绍栈的基本知识,关于栈的应用部分,这里不做更多展开。
表达式求值:书 52-57
括号匹配:Java_300_Day15