栈和队列

栈和队列

栈和队列是两种重要的线性结构。从数据结构的角度上看,栈和队列也是链表,其特殊性在于栈和队列的基本操作是线性表操作的子集,它们是操作受限的线性表,因此可称为限定性的数据结构。但从数据类型的角度上看,它们是和线性表不相同的两类重要的抽象数据类型。本章除了讨论栈和队列的定义、表示方法和实现外,还将给出一些应用的例子。

3.1 栈和队列的定义和特点

3.1.1 栈的定义和特点
栈是限定仅在表尾进行插入或删除操作的线性表。因此,对栈来说,表尾端有其特殊含义,称为栈顶,相应地,表头端称为栈底。而不含元素的空表称为空栈。
假设栈S=(a1,a2,a3…,an),则称a1为栈底元素,an为栈顶元素。栈中元素按a1,a2,a3,…,an的次序进栈,退栈的第一个元素应为栈顶元素。换句话说,栈的修改是按后进先出的原则进行的。因此,栈又称为后进先出的线性表,在日常生活中,有很多类似栈的例子。例如:洗干净的盘子总是逐个往上叠放在已经洗好的盘子上面,而用时从上往下逐个使用。栈的操作特点正是上述实际应用的抽象。在程序设计中,如果需要按照保存数据时相反的顺序来使用数据,则可以利用栈来实现。
3.1.2 队列的定义和特点
相反,队列是一种先进先出线性表,它只允许在表的一端进行插入,而在另一端删除元素。这和日常生活中的排队是一致的,最早进入队列的元素最早离开。
在队列中,允许插入的一端称为队尾,允许删除的一端称为队头。假设队列为q=(a1,a2,a3…,an),那么a1就是队头元素,an就是队尾元素,。队列中的元素是按a1,a2,a3,a4,an的顺序进入的,那么退出队列也只能按照这个次序依次退出,也就是说,只有在a1,a2,…,an-1都离开队列之后,按an才能退出队列。
队列在程序设计中也经常出现。一个最典型的例子就是操作系统中的作业排队,在允许多道程序运行的计算机系统中,同时有几个作业运行。如果运行的结果都需要通道输出,那就要按请求输入的先后次序排队,每当通道传输完毕可以接受新的输出任务时,队头的作业先从队列中退出做输出的操作。凡是申请输出的作业都从队尾进入队列。

3.2 案例

1、表达式求值
表达式求值是程序设计语言编译中的一个最基本的问题,其实现是栈应用的又一个典型例子。“算符优先法”,是一种简单直观、广为使用的表达式求值算法。
要把一个表达式翻译成正确求值的一个机器指令序列,或者直接对表达式求值,首先要能够正确解释表达式。算符优先法就是根据算术四则运算规则确定的运算优先关系,实现对表达式的编译或解释执行的。
在表达式计算中先出现的运算符不一定先运算,具体运算顺序是需要通过运算符优先关系的比较,确定合适的运算时机,而运算时机的确定是通过栈来完成的。将扫描到的不能进行运算的运算数和运算符,先分别压入运算数栈和运算符栈中,在条件满足时再分别从栈中弹出进行运算。
由此可见,这是借助栈的后进后出的特性来处理问题的,在日常生活中,符号先进先出的应用更为常见,例如舞伴问题。
舞伴问题
假设在周末舞会上,男士们和女士们进入舞厅时,各自排成一队。跳舞开始时,依次从男队和女队的队头各出一人配成舞伴。若两队初始人数不相同,则较长的那一队中未配对者等待下一轮舞曲,现要求写一算法模拟上述舞伴配对问题。
先入队的男士或女士应先出队配成舞伴,因此该问题具有先进先出特性,可用队列作为算法的数据结构。
从上面的应用案例可以看出,不论是借助栈还是队列来解决问题,其基本操作都是“入”和“出”。对于栈,在栈顶插入元素的操作称作入栈,删除栈顶元素的操作称为“出栈”;对于队列,在队尾插入元素的操作称为“入队”,在队头删除元素的操作称作“出队”。和线性表一样,栈和队列的存储结构包括顺序和链式两种。

3.3 栈的表示和操作的实现

3.3.1 栈的类型定义
栈的基本操作除了入栈和出栈外,还有栈的初始化、栈空的判定,以及取栈顶元素等。下面给出栈的抽象数据类型定义:
ADT Stack{
数据对象:D={ai|ai∈ElemSet,i=1,2,…,n,n>=0}
数据关系:R={|ai-1,ai∈D,i=1,2,…,n}
约定an为栈顶,ai为栈底。
基本操作:
InitStack(&S)
操作结果:构造一个空栈S。
DestroyStack(&S)
初始条件:栈S已存在
操作结果:栈S被销毁
ClearStack(&S)
初始条件:栈S已存在
操作结果:将栈S清空
StackEmpty(S)
初始条件:栈S已存在
操作结果:若栈为空栈,则返回true,否则返回false
StackLength(S)
初始条件:栈S已存在
操作结果:返回S的元素的个数,即栈的长度
GetTop(S)
初始条件:栈S已存在且非空。
操作结果:返回S的栈顶元素,不修改栈顶指针
Push(&S,e)
初始条件:栈S已存在
操作结果:插入元素e为新的栈顶元素
Pop(&S,&e)
初始条件:栈S已存在且非空
操作结果:删除S的栈顶元素,并用e 返回其值
StackTraverse(S)
初始条件:栈S已存在且非空
操作结果:从栈底到栈顶依次对S中的每个数据进行访问
}ADT Stack
栈的数据元素类型在应用程序内定义。
和线性表相似,栈也有两种存储表示方法,分别称为顺序栈和链栈。
3.3.2 顺序表的表示和实现
顺序栈是指利用顺序存储结构实现的栈,即利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针top指示栈顶元素在顺序表中的位置。通常习惯做法是:以top=0表示空栈,鉴于C语言中的数组下标约定从0开始,则当以C语言作描述语言时,如此设定会带来很大的不便,因此另设指针base指示栈底元素在顺序栈中的位置。当top和base的值相等时,表示空栈。顺序表的定义如下:

//----顺序栈的存储结构-----
#define MAXSIZE 100
typedef struct
{
  SElemType *base;   //栈底指针
  SElemType *top;    //栈顶指针
  int stacksize;     //栈可用的最大容量
}SqStack;

(1)、base为栈底指针,初始化完成后,栈底指针base始终指向栈底的位置,若base的值为NULL,则表明栈结构存在。top为栈顶指针,其初值指向栈底。每当插入新的栈顶元素时,指针top增1;删除栈顶元素时,指针top减一。因此,栈空时,top和base的值相等,都指向栈底;栈非空时,top始终指向栈顶元素的上一个位置。
(2)、stacksize指示栈可使用的最大容量,后面算法的初始化操作为顺序栈动态分配MAXSIZE大小的数组空间,将stacksize置为MAXSIZE。
由于顺序栈的插入和删除只能在栈顶进行,因此顺序栈的基本操作比顺序表要简单得多,以下给出顺序栈部分操作的实现。
1、初始化
顺序栈的初始化操作就是为顺序栈动态分配一个预定义大小的数组空间。
算法 3.1 顺序栈的初始化
【算法步骤】

  1. 为顺序栈动态分配一个最大容量的MAXSIZE的数组空间,使base指向这段空间的基地址,即栈底。
  2. 栈顶指针top初始为base,表示栈为空。
  3. stacksize置为栈的最大容量MAXSIZE。
    【算法描述】
Status InitStack(SqStack &S)
{   //构造一个空栈S
    s.base=new SElemType[MAXSIZE];//为顺序栈动态分配一个最大容量的存储空间
    if(!S.base)
    exit(OVERFLOW);  //存储分配失败
    S.top=S.base;    //top初始为base,其为空栈
    S.stacksize=MAXSIZE;//Stacksize置为栈的最大容量MAXSIZE
    retuen OK;
}  

2.入栈
入栈操作是指在栈顶插入一个新的元素。
算法 3.2 顺序栈的入栈
【算法步骤】

  1. 判断栈是否满,若满则返回ERROR
  2. 将新元素压入栈顶,栈顶指针加1
    【算法描述】
Status Push(SqStack &S,SElemType e)
{  //插入元素e为新的栈顶元素
    if(S.top-S.base==S.stacksize)
    return ERROR;       //栈满
    *S.top++=e;         //元素e压入栈顶,栈顶指针加1
    return OK;
}

3 . 出栈
【算法步骤】

  1. 判断栈是否为空,若空则返回ERROR
  2. 栈顶指针减1,栈顶元素出栈
Status Pop(SqStack &S,SElemType &e)
{
  if(S.top=S.base)      //栈空
  return ERROR;
  e=*--S.top;  //栈顶指针减1,将栈顶元素赋给e
  return OK;
}

4.取出栈顶元素
当栈非空时,此操作返回当前栈顶元素的值,栈顶指针保持不变
算法 3.4 取顺序栈的栈顶元素
【算法描述】

SqStack GetTop(SqStack S)
{  //返回S的栈顶元素,不修改栈顶指针
    if(S.top!=S.base)   //栈非空
    return *(S.top-1);  //返回栈顶元素的值,栈顶指针不变
}  

由于顺序栈和顺序表一样,收到最大空间容量的限制,虽然可以在“满员”时重新分配空间扩大容量,但工作量较大,应该尽量避免。因此在应用程序无法预计栈可能达到的最大容量时,还是应该使用下面介绍的链栈。
3.3.3 链栈的表示和实现
链栈是指采用链式存储结构的。通常链栈用大链表表示,链栈的结点结构与单链表的结构相同,在此用StackNode表示,定义如下:

//-----链式的存储结构----
typedef struct StackNode
{
    ElemType  data;
    struct StackNode *next;
}StackNode,LinkStack;

由于链栈的主要操作是在栈顶插入和删除,显然以链表的头部作为栈顶是最方便的,而且没有必要像单链表那样为了操作方便附加一个头结点。
下面给出链栈部分操作的实现:
1 . 初始化
链栈的初始化操作就是构造一个空栈,因为没有必要设头结点,所以直接将栈顶指针置空即可。
算法 3.5 链栈的初始化
【算法描述】

Status InitStack(LinkStack &S,SElemType &e)
{  //构造一个空栈S,栈顶指针置空
    S=NULL;
    return OK;
}

2、入栈
和顺序栈的入栈操作不同的是,链栈在入栈前不需要判断栈是否满,只需要为入栈元素动态分配一个结点空间。
算法 3.6 链表的入栈
【算法步骤】
1、为入栈元素e分配空间,用指针p指向。
2、将新结点数据域置为e。
3、将新结点插入栈顶。
4、修改栈顶指针为p
【算法描述】

Status Push(LinkStack &S,SElemType e)
{  //在栈顶插入元素
    p=new StackNode;    //生成新结点
    p->data=e;          //将新结点的数据域置为e
    p->next=S;          //将新结点插入栈顶
    S=p;                //修改栈顶指针为p
    return OK;
}

3. 出栈
和顺序表一样,链栈在出栈前也需要判断栈是否为空,不同的是,链栈在出栈后需要释放出栈元素的栈顶空间。
算法 3.7
链表的出栈
【算法步骤】
1、判断栈是否为空,若空则返回ERROR
2、将栈顶元素赋给e
3、临时保存栈顶元素的空间,以备释放
4、修改栈顶指针,指向新的栈顶元素
5、释放原栈顶元素的空间
【算法描述】

Status Pop(LinkStack &S,SElemType &e)
{
     if(S==NULL)
     return ERROE;    //栈空
     e=S->data;       //将栈顶元素赋值给e
     p=S;             //用p临时保存栈顶元素的空间
     S=S->next;       //修改栈顶指针
     delete p;        //释放原栈顶元素的空间
     return Ok;
}  

4、取栈顶元素
和顺序栈一样,当栈非空的时候,此操作返回当前栈顶元素的值,栈顶指针S保持不变。
算法 3.8 取元素的栈顶元素
【算法描述】

SElemType GetTop(LinkList S)
{   //返回S的栈顶元素,不修改栈顶指针
     if(S!=NULL)
     return S->data;
}      

你可能感兴趣的:(栈和队列,数据结构,数据结构,队列)