栈stack是存放数据对象的一种特殊容器,其中的数据元素按线性逻辑次序排列,故可定义首末元素。尽管栈结构也支持对象的插入和删除,但其操作仅限于栈的某一特定端。即新的元素只能从一端插入,或者只能从这一端删除已有元素。栈中可操作的一端称为栈顶,而另一无法直接操作的盲端称为栈底。栈中元素接收操作的次序是先进后出的。栈所支持的操作接口如下:
size() //报告栈的规模 push(e) //将e插至栈顶
empty() //判断栈是否为空 pop() //删除栈顶对象
top() //引用栈顶对象
基于向量的定义可以实现栈结构,栈是向量的派生类,可利用C++继承机制,stack模版类的代码如下:
#include”../Vector/Vector.h” //以向量为基类,派生出栈模版类
template class Stack: public Vector//将向量的首(末)端作为栈底(顶)
{
public: //沿用size()和empty()接口
void push(T const &e)
{
insert(size(),e);//入栈,等效于将新元素作为向量的末元素插入
}
T pop() //出栈,等效于删除向量的末元素
{return remove(size()-1);}
T& top() //取顶,直接返回向量的末元素
{return (*this)[size()-1];}
};
也可直接基于List模版类派生出Stack类。
一、逆序输出
在栈所擅长解决的典型问题中,有一类具有以下共同特征:首先,虽有明确的算法,但其解答却以线性序列的形式给出;其次,无论是递归还是迭代实现,该序列都是逆序输出的;最后,输入和输出规模不确定,难以事先确定盛放输出数据的容器大小。
例 2.3.1进制转换算法
给定任意十进制整数n,将其转化为 进制的表示形式。
12345 (10)=30071 (8)若计ni=(dm...di)(λ)则di%λ,ni+1=ni/λ
递归实现 |
迭代实现 |
|
|
二、递归嵌套
具有自相似性的问题多可嵌套地递归描述,但因嵌套位置和嵌套深度并不固定,其递归算法的复杂度难以控制。栈结构及其操作天然地具有递归嵌套性,故可以高效地解决这类问题。
若有三个栈A,B,S,A自顶向下构成序列{a 1,a 2,...a n},B和S为空。若只允许S,push(A.pop())弹出A栈的元素并压入S栈,或通过B.push(S.pop())弹出S栈元素并压入B栈,当A和S均为空时,A栈中的元素均已压入B栈(这两个过程都是随机发生的)。此时,将B中元素自底向上构成的序列记为:{a k 1,a k 2,...a k n},则称该序列为原输入序列的一个 栈混洗。对于长度为n的输入序列,每一个栈混洗都对应于栈S的n次push和n次pop组成的操作序列。例 2.3.2括号匹配
对表达式括号匹配的检查是语法检查中一个很重要的环节。其任务是,对任意程序块,判断其中的括号是否在嵌套的意义下完全匹配。例如下式中前者匹配,而后者不匹配。
a/(b[i-1][j+1]+c[i+1][j-1])*2
a/(b[i-1][j+1])+c[i+1][j-1])*2分析:
不妨只考虑圆括号,用“+”表示字符串的接续,则表达式S一般可以表示如下:
S=S0+"("+S1+")"+S2+S3void trim(const char exp[], int& lo, int& hi)//删除表达式exp[lo,hi]不含括号的最长前后缀
{
//查找第一个和最后一个括号
while((lo<=hi)&&(exp[lo]!=’(‘)&&(exp[lo]!=’)’)) lo++;
while((lo<=hi)&&(exp[hi]!=’(‘)&&(exp[hi]!=’)’)) hi++;
}
int divide(const char exp[], int lo,int hi)//切分表达式exp[lo,hi]
{
int mi=lo;int crc=1;//crc为[lo,hi]范围内左右括号数之差,第一个字符是’(‘,因此crc=1
while((0hi) return true;//清除不含括号的前缀和后缀
if (exp[lo]!=’(‘) return false;//首字符非左括号,则必不匹配
if (exp[hi]!=’)’) return false;
int mi=divide(exp,lo,hi);//确定适当的切分点
if (mi>hi) return false;
return paren(exp,lo+1,mi-1)&&paren(exp,mi+1,hi);//分别检查左右子表达式
}
使用递归的方法时间复杂度很大,且诶难以处理多种括号的表达式,应该做进一步优化。使用push、pop等操作分别与左右括号相对应,则长度为n的栈混洗必然与由n对括号组成的合法表达式彼此对应。paren()函数的改进如下:
bool paren(const char exp[],int lo,int hi)
{
Stack S;//用栈记录已发现但尚未匹配的左括号
for (int i=0;exp[i];i++) //逐一检查当前字符
switch(exp[i]) //左括号直接进栈;右括号若与栈顶不匹配,则表达式必不匹配
{
case ‘(‘: S.push(exp[i]);break;
case ‘[‘: S.push(exp[i]);break;
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();//若栈中仍残留括号,则不匹配
}
与栈一样,队列queue也是存放数据对象的一种容器,其中的数据对象也按照线性逻辑次序排列。队列结构同样支持对象的插入和删除,但新对象只能从某一端插入其中,从另一端删除已有元素。允许取出元素的一端称为队头,而允许插入元素的另一端称为队尾。这些操作称为入队和出队,可以发现队列中对象的操作次序遵循“先进先出”这一准则。队列的ADT接口如下所示:
size() //报告队列的规模 empty() //判断队列是否为空
enqueue(e) //将e插入队尾 dequeue() //删除队首对象
front() //引用队首对象
队列可以视为列表的派生类,可利用C++的继承机制,基于列表模版类实现队列的结构。队列的模版类如下:
#include”../List/List.h”
template
class Queue: public List //继承List类的原有接口
{
public:
void enqueue(T const &e) {insertAsLast(e);}//入队表示的是列表的尾部插入
T dequeue() {return remove(first());} //出队表示的是删除首部元素
T& front() {return first()->data;}
};
size()和empty()接口均可沿用基类的同名接口。
一、循环分配器
在客户群体中共享某一资源,比如多个应用程序共享一个CPU,队列结构则非常适用于定义和实现这样一套分配规则。具体地,可以借助队列Q实现一个资源循环分配器,具体代码如下:
RoundRobin //循环分配器(轮值算法)
{
Queue Q(clients); //所有参与资源分配的用户组成队列Q
while(!ServiceClosed()) //服务器关闭之前
{
e=Q.dequeue(); serve(e);//队首的用户出队并接受服务
Q.enqueue(e);//重新入队
}
}
二、银行服务模拟
通常,银行设有多个窗口,顾客按照到达的次序分别在各窗口排队等待办理业务。为此可以定义顾客类Customer如下,记录顾客所属的队列及其所办业务的服务时长。
struct Customer
{
int window;//所属窗口(队列)
unsigned int time;//服务时长
}
void simulate(int nWin, int servTime) //按指定窗口数、服务总时间模拟银行业务
{
Queue* windows=new Queue[nWin];//为每一窗口创建一个队列
for (int now=0;now windows[], int nWin)
{
int minSize=windows[0].size(), optWin=0;//最优队列
for (int i=1;iwindows[i].size())
{minSize=windows[i]/size();optWin=i;}
return optWin;
}