C++及数据结构复习笔记(十二)(栈与队列)

2.3 栈与队列

2.3.1 栈的ADT接口

       栈stack是存放数据对象的一种特殊容器,其中的数据元素按线性逻辑次序排列,故可定义首末元素。尽管栈结构也支持对象的插入和删除,但其操作仅限于栈的某一特定端。即新的元素只能从一端插入,或者只能从这一端删除已有元素。栈中可操作的一端称为栈顶,而另一无法直接操作的盲端称为栈底。栈中元素接收操作的次序是先进后出的。栈所支持的操作接口如下:

size()   //报告栈的规模             push(e) //将e插至栈顶

empty()  //判断栈是否为空          pop() //删除栈顶对象

top()  //引用栈顶对象

       基于向量的定义可以实现栈结构,栈是向量的派生类,可利用C++继承机制,stack模版类的代码如下:

#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];}
};

       也可直接基于List模版类派生出Stack类。

2.3.2 栈的典型应用

一、逆序输出

       在栈所擅长解决的典型问题中,有一类具有以下共同特征:首先,虽有明确的算法,但其解答却以线性序列的形式给出;其次,无论是递归还是迭代实现,该序列都是逆序输出的;最后,输入和输出规模不确定,难以事先确定盛放输出数据的容器大小。

例 2.3.1进制转换算法

       给定任意十进制整数n,将其转化为 进制的表示形式。

       12345(10)=30071(8)
       一般地,设n=(dm...d2d1)(λ)=dm×(λ)^m+...d2×(λ)^2+d1×(λ)^1

       若计ni=(dm...di)(λ)则di%λ,ni+1=ni/λ

递归实现

迭代实现

void convert(Stack<char>& S, _init64 n, int base)
//十进制数n到base进制的转换
{
  static char digit[]= {‘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]);
//高数位保存在栈底
  }
}
void convert(Stack<char>& S, +int64 n, int base)
{
  static char digit[]= {‘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;
}
}

二、递归嵌套

       具有自相似性的问题多可嵌套地递归描述,但因嵌套位置和嵌套深度并不固定,其递归算法的复杂度难以控制。栈结构及其操作天然地具有递归嵌套性,故可以高效地解决这类问题。

       若有三个栈A,B,S,A自顶向下构成序列{a1,a2,...an},B和S为空。若只允许S,push(A.pop())弹出A栈的元素并压入S栈,或通过B.push(S.pop())弹出S栈元素并压入B栈,当A和S均为空时,A栈中的元素均已压入B栈(这两个过程都是随机发生的)。此时,将B中元素自底向上构成的序列记为:{ak1,ak2,...akn},则称该序列为原输入序列的一个栈混洗。对于长度为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+S3
void 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((0<crc)&&(++mi<hi))//逐个检查各字符,直到左右括号数目相等或者越界
  {
if (exp[mi]==’)’) crc--;
if(exp[mi]==’(‘) crc++;
//左右括号分别计数
  }
return mi;//若mi<=hi,则为合法切分点,否则认为局部不可能匹配,返回的mi为最后一个右括号位置
}
bool paren(const char exp[],int lo,int hi)//检查表达式exp[lo,hi]是否括号匹配
{
  trim(exp,lo,hi); if(lo>hi) 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<char> 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();//若栈中仍残留括号,则不匹配
}

2.3.3 队列

       与栈一样,队列queue也是存放数据对象的一种容器,其中的数据对象也按照线性逻辑次序排列。队列结构同样支持对象的插入和删除,但新对象只能从某一端插入其中,从另一端删除已有元素。允许取出元素的一端称为队头,而允许插入元素的另一端称为队尾。这些操作称为入队和出队,可以发现队列中对象的操作次序遵循“先进先出”这一准则。队列的ADT接口如下所示:

size()  //报告队列的规模            empty()    //判断队列是否为空

enqueue(e)  //将e插入队尾        dequeue()  //删除队首对象

front()    //引用队首对象

       队列可以视为列表的派生类,可利用C++的继承机制,基于列表模版类实现队列的结构。队列的模版类如下:

#include”../List/List.h”
template <typename T>
class Queue: public List<T> //继承List类的原有接口
{
  public:
void enqueue(T const &e) {insertAsLast(e);}//入队表示的是列表的尾部插入
T dequeue() {return remove(first());}  //出队表示的是删除首部元素
T& front() {return first()->data;}
};

       size()和empty()接口均可沿用基类的同名接口。

2.3.4 队列的典型用用

一、循环分配器

       在客户群体中共享某一资源,比如多个应用程序共享一个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<Customer>* windows=new Queue<Customer>[nWin];//为每一窗口创建一个队列
  for (int now=0;now<servTime;now++)
  //在下班之前,每隔一个单位时间进行如下操作
  {
if (rand()%(1+nWin)) //新顾客以nWin/(nWin+1)的概率到达
{
  Custom c; c.time=1+rand()%98;//新客户到达,服务时长随机确定
      c.window=bestWindow(windows,nWin)//找到最佳服务接口,即当前最短队列
  windows[c.window].enqueue(c);//新乘客加入相应队列
}
for(int i=0;i<nWin;i++)//分别检查
  if(!windows[i].empty())//若各队列非空
    if (--windows[i].front().time<=0)//队首顾客的服务时长减少一个单位
      window[i].dequeue();
}
delete [] windows;
}
//查找最短队列
int bestWindow(Queue<Customer> windows[], int nWin)
{
  int minSize=windows[0].size(), optWin=0;//最优队列
  for (int i=1;i<nWin;i++)
    if(minSize>windows[i].size())
{minSize=windows[i]/size();optWin=i;}
  return optWin;
}
展开阅读全文

没有更多推荐了,返回首页