3.0 栈和队列
顺序栈、链栈、循环队列、链队列
- 学习目标
掌握栈和队列这两种抽象数据类型的特点,并能在相应的应用问题中正确选用它们。
熟练掌握栈类型的两种实现方法。
熟练掌握循环队列和链队列的基本操作实现算法。
理解递归算法执行过程中栈的状态变化过程。 - 知识点
顺序栈、链栈、循环队列、链队列
栈
只允许在一端插入和删除的顺序表
允许插入和删除的一端称为栈顶 (top) —表尾
另一端称为栈底(bottom) —表头
不含元素的空表称空栈
特点: 先进后出(FILO)或后进先出(LIFO)
ADT
ADT Stack
{
数据对象:D={ai|ai∈ElemSet,i=1,2,…,n,n≥0}
数据关系:R={<ai-1,ai>|ai,ai-1∈D,i=2,…,n}
约定:an为栈顶,a1为栈底
基本操作:
InitStack(&S)
操作结果:构造一个空的栈S。
DestroyStack(&S)
初始条件: 栈S已经存在。
操作结果: 销毁栈S。
ClearStack(&S)
初始条件: 栈S已经存在。
操作结果: 将栈S重置为空栈。
StackEmpty(S)
初始条件: 栈S已经存在。
操作结果: 若栈S为空栈,则返回TURE;否则返回FALSE
-判定栈是否为空栈是栈在应用程序中经常使用的操作,通常以它作为循环结束的条件。
StackLength(S)
初始条件: 栈S已经存在。
操作结果: 返回栈S中的数据元素个数。
GetTop(S,&e)
初始条件: 栈S已经存在且非空。
操作结果: 用e返回栈S中栈顶元素的值。
-这是取栈顶元素的操作,只以 e 返回栈顶元素,并不将它从栈中删除。
Push(&S,e)
初始条件: 栈S已经存在。
操作结果: 插入元素e为新的栈顶元素。
-这是入栈操作,在当前的栈顶元素之后插入新的栈顶元素。
Pop(&S,&e)
初始条件: 栈S已经存在且非空。
操作结果: 删除S的栈顶元素并用e返回其值。
-这是出栈操作,不仅以 e 返回栈顶元素,并将它从栈中删除。
StackTraverse(S,visit ())
初始条件: 栈S已经存在且非空。
操作结果: 从栈底到栈顶依次对S的每个元素调用函数visit ()。一旦visit ()失败,则操作失败。
-这是对栈进行从栈底到栈顶的"遍历"操作,应用较多的场合是,输出栈中所有数据元素。
}
栈的表示和实现
两种
- 顺序存储结构__顺序栈;
- 链式存储结构__链栈;
顺序栈的定义
利用一组地址连续的存储单元依次自栈底到栈顶存放栈的数据元素。
- 栈顶指针top
指向实际栈顶后的空位置,初值为0 - top=0,栈空, 此时出栈,则下溢(underflow)
top=M,栈满,此时入栈,则上溢(overflow)
顺序栈的数据类型
#define STACK_INIT_SIZE 100;//存储空间初始分配量
#define STACKINCREMENT 10;//存储空间分配增量
typedef struct
{
SElemType *base; //在栈构造之前和销毁之后,值为null
SElemType *top; //栈顶指针
int StackSize; //当前已分配的存储空间,以元素为单位
} SqStack;
顺序栈的操作
InitStack
Status InitStack( SqStack &S )
{
S.Base=(SElemType *)malloc(STACK_INIT_SIZE*sizeof(SElemType));
if(!S.Base)
{
return OVERFLOW;
}
S.Top = S.Base;
S.StackSize = STACK_INIT_SIZE;
return OK;
}// InitStack
GetTop
// 用e返回栈S的栈顶元素,若栈空,函数返回ERROR
Status GetTop( SqStack S, SElemType &e)
{
if( S.Top != S.Base ) // 栈空吗?
{
e = *( S.Top – 1 );
return OK;
}
else
{
return ERROR;
}
}// GetTop
Push
//把元素e入栈
Status Push(SqStack &S, SElemType e )
{
// 若栈满,追加存储空间
if( S.Top >= S.Base + S.StackSize )
{
S.Base= (SElemType *)realloc(S.Base,(S.StackSize + STACKINCREMENT) *sizeof(SElemType));
if( !S.Base )
return OVERFLOW; //存储分配失败
S.Top = S.Base + S.StackSize;
S.StackSize += STACKINCREMENT;
}
*S.Top = e;
S.Top++;
return OK;
}// Push
Pop
// 出栈
Status Pop( SqStack &S, SElemType &e )
{
if( S.Top == S.Base ) // 空吗?
{
return ERROR;
}
S.Top --;
e = *S.Top;
return OK;
}// Pop
链栈
定义
typedef struct{
SLink top; // 栈顶指针
int length; // 栈中元素个数
}Stack;
typedef struct node{
int data;
struct node *next;
}*SLink;
栈的应用举例
把10进制数159转换成8进制数
因此,需要先保存在计算过程中得到的八进制数的各位,然后逆序输出,因为它是按"后进先出"的规律进行的,所以用栈最合适。
void conversion ()
{// 对于输入的任意一个非负十进制整数,打印输出与其等值的八进制数
InitStack(S); // 构造空栈
scanf ("%d",N);
while (N) {
Push(S, N % 8);
N = N/8;
}
while (!StackEmpty(S)) {
Pop(S,e);
printf ( "%d", e );
}
} // conversion
括弧匹配检验
现在的问题是,要求检验一个给定表达式中的括弧是否正确匹配?
检验括号是否匹配的方法可用“期待的急迫程度”这个概念来描述。
status matching(string& exp) {
// 检验表达式中所含括弧是否正确嵌套,若是,则返回
// OK,否则返回ERROR
int state = 1;
while (i<=length(exp) && state) {
swith of exp[i] {
case "(": {Push(S,exp[i]); i++; break;}
case ")": {
if (NOT StackEmpty(S) && GetTop(S) = "(")
{ Pop(S,e); i++; }
else{ state = 0 }
break;
}
… }
}
if ( state && StackEmpty(S) )
return OK
else return ERROR;
}
行编辑程序问题
- 一个简单的行编辑程序的功能是:接受用户从终端输入的程序或数据,并存入用户的数据区。每接受一个字符即存入用户数据区。
- 较好的做法是,设立一个输入缓冲区,用以接受用户输入的一行字符,然后逐行存入用户数据区。允许用户输入出差错,并在发现有误时可以及时更正。
例如,可用一个退格符“#”表示前一个字符无效;可用一个退行符“@”,表示当前行中的字符均无效。
例如,假设从终端接受了这样两行字符:
whli##ilr#e(s#s)
outcha@putchar(s=#++);
则实际有效的是下列两行:
while (s)
putchar(s++);
void LineEdit() {
// 利用字符栈S,从终端接收一行并传送至调用过程
// 的数据区。
InitStack(S); //构造空栈S
ch = getchar(); //从终端接收第一个字符
while (ch != EOF) { //EOF为全文结束符
while (ch != EOF && ch != '\n') {
switch (ch) {
case '#' : Pop(S, c); break; // 仅当栈非空时退栈
case '@': ClearStack(S); break; // 重置S为空栈
default : Push(S, ch); break; // 有效字符进栈,未考虑栈满情形
}
ch = getchar(); // 从终端接收下一个字符
}
将从栈底到栈顶的字符传送至调用过程的数据区
ClearStack(S); // 重置S为空栈
if (ch != EOF) ch = getchar();
}
DestroyStack(S);
}
表达式求值
- 规则:
先乘除,后加减;
从左到右;
先括号内,后括号外;
- 把运算符和界限符统称为算符
- “算符优先法”
4 - 10 / 5 + 2 * ( 3 + 8 )
4
4 -
4 - 10
4 - 10 /
4 - 10 / 5
4 - 10 / 5 + => 4 – 2 +
4 - 2 + => 2 +
2 + 2
2 + 2 *
2 + 2 * (
2 + 2 * ( 3
2 + 2 * ( 3 +
2 + 2 * ( 3 + 8
2 + 2 * ( 3 + 8 ) => 2 + 2 * 11 => 2 + 22 => 26
算符优先法
根据这个运算优先关系的规定来实现对表达式的编译或解释执行。
算符间的优先关系
算法描述
//表达式求值
OpendType EvaluateExpression( ){
InitStack( OPTR );
Push( OPTR, ‘#’ );
InitStack( OPND );
c = getchar( );
while(!(c == ‘#’ && GetTop( OPTR ) == ‘#’) ){
if(!In(c,OP)) // c是运算符?,OP是运算符集合
{
Push( OPND, c);
c = getchar( );
}
else
{
switch( Precede ( GetTop( OPTR), c ))
{
case ‘<’ : //栈顶元素优先权低
Push( OPTR, c );
c = getchar( );
break;
case ‘=‘ : // c为’)’
Pop( OPTR, x );
c = getchar( );
break;
case ‘>’: //退栈并将运算结果入栈
Pop( OPTR, t );
Pop( OPND, b );
Pop( OPND, a );
Push( OPND, Operate( a, t, b ));
break;
}// switch
}// while
return GetTop( OPND );
}// EvaluateExpression
过程的嵌套调用
……
汉诺塔问题
……
队列
定义
一种先进先出的线形表。只允许在表一端插入,在另一端删除。
- 概念
队尾rear:插入端,线性表的表尾。
队头front:删除端,线性表的表头。
FIFO(First In First Out)
ADT
ADT Queue
{
数据对象:D={ai|ai∈ElemSet,i=1,2,…,n,n≥0}
数据关系:R={<ai-1,ai>|ai,ai-1∈D,i=2,…,n}
约定:a1为队列头,an为队列尾
基本操作:
InitQueue( &Q ); // 初始化空队列
DestroyQueue( &Q ); // 销毁队列
ClearQueue( &Q ); // 清空队列
QueueEmpty( Q ); // 队列空?
QueueLength( Q ); // 队列长度
GetHead( Q, &e ); // 取对头元素
EnQueue( &Q, e ); // 入队列
DeQueue( &Q, &e ); // 出队列
QueueTraverse( Q, visit( )); // 遍历
}//ADT Queue;
双端队列简介
可以从两端进行插入或者删除操作的队列。
队列的表示和实现
两种
- 链式存储结构__链队列;
- 顺序存储结构__循环队列;
链队列
类型说明
typedef struct Qnode {
QElemType data;
struct QNode *next;
}QNode,*QueuePtr; // 结点
typedef struct{
QueuePtr fornt; // 队头指针
QueuePtr rear; // 队尾指针
}LinkQueue;
结点定义
typedef struct Qnode{
QElemType data;
struct QNode *Next;
}QNode
链队列的基本操作
初始化
// 初始化一个空队列
Status InitQueue( LinkQueue &Q )
{
Q.Front=Q.Rear=(QueuePtr)malloc(sizeof(QNode));
if(!Q.front)
{
return OVERFLOW;
}
Q.front->Next=NULL;
}//InitQueue
销毁
// 销毁队列
Status DestroyQueue(LinkQueue &Q){
while(Q.Front){
QRear = Q.Front ->next;
free( Q.Front );
Q.Front= QRear
}
return OK;
}// DestroyQueue
入队
// 入队列
Stauts EnQueue( LinkQueue &Q, QEmemType e )
{
p=(QueuePtr)malloc(sizeof(QNode));
if( !p){return OVERFLOW; }//存储分配失败
p->data = e; p->next = null ;
Q.Rear->next = p;
Q.Rear=p;
return OK;
}
出队
// 出队列 3-3-12-1.swf
Status DeQueue( LinkQueue &Q, QElemType &e )
{
if(Q.Front==Q.rear)
return ERROR;
p = Q.Front->Next ;//p指向队头
e = p->data;//取队头元素值(可以直接用e完成对队头取值)
Q.Front->next = p->next;//头结点指向原队头的下一个节点
if(Q.rear==p) //尾头同指向,队列就空了,释放队列
Q.rear=Q.front;
free( p );
return OK;
}// Dequeue
循环队列
》 基本思想:把队列设想成环形,让sq[0]接在sq[M-1]之后,若rear+1==M,则令rear=0;
》实现:利用“模”运算
》入队: rear=(rear+1)%M; sq[rear]=x;
》出队: front=(front+1)%M; x=sq[front];
》队满、队空判定条件
- 会存在判断队空和队满时,条件均为front==rear的情况
- 解决方案:少用一个元素空间
队空:front= =rear
队满:(rear+1)%M= =front
约定以“队列头指针在队列尾指针的下一位置上”作为队列呈“满”状态的标志,在同一位置则是空。
循环队列的基本操作
基本模块说明
#define MAXQSIZE 100 //队列最大长度
typedef struct
{
QElemType *Base; // pBase指向数组名(通常静态队列都使用循环队列)
int Front; // 头指针,数组下标,此处规定从零开始
int Rear; // 尾指针
}SqQueue;
初始化
Status InitQueue( SqQueue &Q ){
Q.Base = (QElemTYpe *)malloc( MAXQSIZE * sizeof( QElemType ));
if( !Q.Base ){
return OVERFLOW;}
Q.Front = Q.Rear = 0;
return OK;
}// InitQueue
队列长度
int QueueLength( SqQueue Q )
{
return Q.Rear – Q.Front + MAXQSIZE) % MAXQSIZE;
}
3 入队列
Status EnQueue( SqQueue &Q, QElemType e ){
if( (Q.Rear + 1)%MAXQSIZE == Q.Front ){
return ERROR;} // 队列满?
Q.Base[Q.Rear] = e;
Q.Rear = ( Q.Rear + 1 ) % MAXQSIZE;
}// EnQueue
出队列
Status DeQueue( SqQueue &Q, QElemType &e ){
if( Q.Front == Q.Rear ){
return ERROR;} // 对列空?
e = Q.Base[Q.Front];
Q.Front = (Q.Front + 1) % MAXQSIZE;
return OK;
}// DeQueue;
队列应用举例
由事件驱动的程序
银行业务模拟系统
- 顾客到达银行
选择一个人数最少的柜台排队,如果该柜台没有人,则直接开始办理业务,并准备该顾客的业务完成事件。
如果未到下班时间,则准备下一个顾客到达银行的事件。 - 顾客业务完成
顾客完成业务离开,下一个顾客开始办理业务,并准备该队列中下一个顾客的业务完成事件。
初始化事件队列;
起始事件(第一个顾客到达事件)入队;
循环,直至事件队列空
事件出队;
若为到达事件,则
选择人数最少的柜台排队;
若该柜台无人排队,则业务完成事件入队;
若未到下班时间,则下一个顾客到达事件入队;
若为业务完成事件,则
顾客离开;
若该柜台还有人排队,则准备下一个人的业务完成事件;