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;

队列应用举例

由事件驱动的程序

银行业务模拟系统

  • 顾客到达银行
    选择一个人数最少的柜台排队,如果该柜台没有人,则直接开始办理业务,并准备该顾客的业务完成事件。
    如果未到下班时间,则准备下一个顾客到达银行的事件。
  • 顾客业务完成
    顾客完成业务离开,下一个顾客开始办理业务,并准备该队列中下一个顾客的业务完成事件。

初始化事件队列;
起始事件(第一个顾客到达事件)入队;
循环,直至事件队列空
事件出队;
若为到达事件,则
选择人数最少的柜台排队;
若该柜台无人排队,则业务完成事件入队;
若未到下班时间,则下一个顾客到达事件入队;
若为业务完成事件,则
顾客离开;
若该柜台还有人排队,则准备下一个人的业务完成事件;

posted @ 2020-08-14 20:42  夜明_Yoake  阅读(171)  评论(0编辑  收藏  举报