CH3-栈和队列

3.1栈和队列的定义和特点

  • 栈和队列是两种常用的、重要的数据结构
  • 栈和队列是限定插入和删除只能在表的“端点”进行的线性表

20220110191610

20220110190514

20220110190827

20220110191732

栈的应用

20220110191159

队列的应用

  • 由于队列的操作具有先进先出的特性,使得队列成为程序设计中解决类似排队问题的有用工具。
    • 脱机打印输出,按申请的先后顺序依次输出
    • ·多用户系统中,多个用户排成队,分时地循环使用CPU和主存
    • 按用户的优先级排成多个队,每个优先级一个队列
    • 实时控制系统中,信号按接收的先后顺序依次处理
    • 网络电文传输,按到达的时间先后顺序依次进行

3.1.1栈的定义和特点

20220110192515

20220110192933

20220110195938

20220110200046

20220110200457

20220110200629

20220110200742

3.1.2队列的定义和特点

20220110200933

20220110201032

3.2案例引入

案例3.1 :进制转换

20220110201151

20220110205307

案例3.2:括号匹配的检验

20220110205721

案例3.3:表达式求值

20220110205922

20220110210221

案例3.4∶舞伴问题

幻灯片30

3.3栈的表示和实现

3.3.1抽象数据类型定义

20220110210836

幻灯片33

由于栈本身就是线性表,于是栈也有顺序存储和链式存储两种实现方式

  • 栈的顺序存储---顺序栈
  • ·栈的链式存储---链栈

3.3.2顺序栈

20220111103055

20220111103258

使用数组作为顺序栈存储方式的特点:

​ 简单方便、但易产生溢出(数组大小固定)

  • 上溢(overflow): 栈已经满,又要压入元素

    • 注:上溢是一种错误,使问题的处理无法进行;
  • 下溢(underflow): 栈已经空,还要弹出元素

    • 而下溢一般认为是—种结束条件,即问题处理结束。

顺序栈的表示

#define MAXSIZE 100
typedef struct{
    SElemType *base;	//栈底指针
    SElemType *top;		//栈顶指针
    int stacksize;		//栈可用最大容量
}SqStack;

20220111104038

【算法3.1】初始化

Status InitStack(SqStack &S){//构造一个空栈
    S.base = new SElemType[MAXSIZE];
    //或S.base = (SElemType*)malloc(MAXSIZE*sizeof(SElemType));
    if (!S.base) exit (OVERFLOW);	//存储分配失败
    S.top= S.base;					//栈顶指针等于栈底指针
	S.stacksize = MAXSIZE;
	return OK;
}

【算法补充】判断栈是否为空

Status StackEmpty(SqStack S)
   // 若栈为空,返回TRUE;否则返回FALSE
    if (S.top == S.base)
    	return TRUE;
	else
    	return FALSE;
}

【算法补充】求栈长

int StackLength( SqStack S)
{
    return S.top - S.base;
}

【算法补充】清空

Status ClearStack( SqStack S ) {
    if( S.base ) 	S.top = S.base;
    return OK;	
}

【算法补充】销毁

Status DestroyStack( SqStack &S ){
    if(S.base ) {
        delete S.base ;
        S.stacksize = O;
        S.base = S.top = NULL;
    }
    return OK;
}

【算法3.2】入栈

  • (1)判断是否栈满,若满则出错(上溢)
  • (2)元素e压入栈顶
  • (3)栈顶指针加1
Status Push( SqStack &S, SElemType e) {
    if( S.top - S.base== S.stacksize )//栈满
   		return ERROR;
    *S.top++=e;				//*S.top=e;	S.top++;
	return OK;
}

【算法3.3】出栈

  • (1)判断是否栈空,若空则出错(下溢)
  • (2)获取栈顶页元素
  • (3)栈顶指针减1
Status Pop(SqStack &S, SElemType &e){
//若栈不空,则删除S的栈顶无素,用e返回其值,并返回OK;否则返回ERROR
    if(S.top == S.base)		//等价于if(StackEmpty(S))
        return ERROR;
    e = *--S.top;			//--S.top;	e=*S.top; 
    retum OK;
}

3.3.3链栈

typedef struct StackNode
{
    SElemType data;
    struct StackNode *next;
}StackNode, *LinkStackPtr;

typedef struct LinkStack
{
    LinkStackPtr top;
    int count;
}*PLinkStack;

20220111111242

【算法3.5】初始化

/*初始化栈*/
Status InitStack(PLinkStack *S)
{
    *S = (PLinkStack)malloc(sizeof(struct LinkStack));
    (*S)->top = NULL;
    (*S)->count = 0;
    return OK;
}

【补充算法】判断栈是否为空

/*判断栈是否为空*/
Status StackEmpty(PLinkStack S)
{
    if(S->top)/*栈存在且非空*/
        return FALSE;
    else
        return TRUE;
}

【算法补充】求栈长

/*查看栈元素个数*/
int StackLength(PLinkStack S)
{
    return S->count;
}

【算法补充】清空

/*清空栈*/
Status ClearStack(PLinkStack S)
{
    LinkStackPtr p;
    while(S->top){
        p = S->top;
        S->top = S->top->next;
        S->count--;
        free(p);
    }
    return OK;
}

【算法补充】销毁

/*销毁栈*/
Status DestroyStack(PLinkStack *S)
{
    ClearStack(*S);
    free(*S);
    return OK;
}

【算法补充】显示元素格式

/*显示元素格式*/
Status visit(SElemType e)
{
    printf("%d ",e);
    return OK;
}

【算法3.6】入栈

/*入栈*/
Status Push(PLinkStack S,SElemType *e)
{
    LinkStackPtr p = (LinkStackPtr)malloc(sizeof(struct StackNode));
    p->data = *e;
    p->next = S->top;
    S->top = p;
    S->count++;
    return OK;
}

【算法3.7】出栈

Status Pop (LinkStack &S,SElemType &e){
    if (S==NULL)	return ERROR;
    e = S-> data;
    p = S;
    S = S-> next;
    delete p;
    return OK;
}

【算法3.8】取栈顶元素

/* 若栈存在且非空,用e返回S的栈顶元素 */
Status GetTop(PLinkStack S,SElemType *e)
{
    if(!S->top)return ERROR;
    *e = S->top->data;
    return OK;
}

【算法3.9】遍历栈

/*遍历栈中所有元素*/
Status StackTraverse(PLinkStack S,Status (* visit)(SElemType))
{
    LinkStackPtr p;
    p = S->top;
    while(p){
        visit(p->data);
        p = p->next;
    }
    printf("\n");
    return OK;
}

主程序测试

/*
栈的抽象数据类型
ADT 栈(stack)
Data
    同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系
Operation
    InitStack(*S):初始化操作,建立一个空栈S
    DestroyStack(*S):若栈存在,则销毁它
    ClearStack(S):将栈清空
    StackEmpty(S):若栈为空,返回true,否则返回false
    GetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素
    Push(S,e):若栈存在,插入新元素e到栈S中并成为栈顶元素
    Pop(S,*e):删除栈S中栈顶元素,并用e返回其值
    StackTraverse(S):遍历栈中所有元素
    StackLength(S):从栈顶遍历所有元素
endADT

 */

#include <stdio.h>
#include <stdlib.h>

#define OVERFLOW        -1
#define STACK_INIT_SIZE 10
#define STACKINCREMENT   2
#define OK               1
#define ERROR            0
#define TRUE             1
#define FALSE            0

typedef int SElemType;
typedef int Status;


typedef struct StackNode
{
    SElemType data;
    struct StackNode *next;
}StackNode, *LinkStackPtr;

typedef struct LinkStack
{
    LinkStackPtr top;
    int count;
}*PLinkStack;

/*初始化栈*/
Status InitStack(PLinkStack *S)
{
    *S = (PLinkStack)malloc(sizeof(struct LinkStack));
    (*S)->top = NULL;
    (*S)->count = 0;
    return OK;
}

/*清空栈*/
Status ClearStack(PLinkStack S)
{
    LinkStackPtr p;
    while(S->top){
        p = S->top;
        S->top = S->top->next;
        S->count--;
        free(p);
    }
    return OK;
}

/*销毁栈*/
Status DestroyStack(PLinkStack *S)
{
    ClearStack(*S);
    free(*S);
    return OK;
}

/*判断栈是否为空*/
Status StackEmpty(PLinkStack S)
{
    if(S->top)/*栈存在且非空*/
        return FALSE;
    else
        return TRUE;
}

/* 若栈存在且非空,用e返回S的栈顶元素 */
Status GetTop(PLinkStack S,SElemType *e)
{
    if(!S->top)return ERROR;
    *e = S->top->data;
    return OK;
}

/*入栈*/
Status Push(PLinkStack S,SElemType *e)
{
    LinkStackPtr p = (LinkStackPtr)malloc(sizeof(struct StackNode));
    p->data = *e;
    p->next = S->top;
    S->top = p;
    S->count++;
    return OK;
}

/*出栈*/
Status Pop(PLinkStack S,SElemType *e)
{
    LinkStackPtr p;
    if(!GetTop(S,e))return ERROR;
    p = S->top;
    S->top = S->top->next;
    S->count--;
    free(p);
    return OK;
}

/*查看栈元素个数*/
int StackLength(PLinkStack S)
{
    return S->count;
}

/*遍历栈中所有元素*/
Status StackTraverse(PLinkStack S,Status (* visit)(SElemType))
{
    LinkStackPtr p;
    p = S->top;
    while(p){
        visit(p->data);
        p = p->next;
    }
    printf("\n");
    return OK;
}

/*显示元素格式*/
Status visit(SElemType e)
{
    printf("%d ",e);
    return OK;
}

int main()
{
    SElemType e,i;
    PLinkStack s;
    printf("InitStack初始化栈并将1--12压入栈\n");
    if(InitStack(&s))
        for(e = 1; e <= 12; e++)
        {
            Push(s,&e);
        }
    printf("StackTraverse栈中元素从栈顶依次为:\n");
    StackTraverse(s,visit);
    Pop(s,&e);
    printf("Pop弹出的元素为:%d\n",e);
    Pop(s,&e);
    printf("Pop又弹出的元素为:%d\n",e);
    printf("Push将刚刚弹出的元素%d再次压入\n",e);
    Push(s,&e);
    printf("StackEmpty判断栈是否为空:%d(1:是 0:不是)\n",StackEmpty(s));
    GetTop(s,&e);
    printf("GetTop当前栈顶元素为:%d\n",e);
    printf("StackLength:当前栈长度为%d \n",StackLength(s));
    ClearStack(s);
    printf("ClearStack栈清空后,StackEmpty栈是否为空%d(1:是 0:不是)\n",StackEmpty(s));
    DestroyStack(&s);
    printf("DestroyStack栈销毁");
    return 0;

}

20220113205349

3.4栈与递归

递归的定义

  • 若一个对象部分地包含它自己 ,或用它自己给自己定义,则称这个对象是递归的;

  • 若一个过程直接地或间接地调用自己,则称这个过程是递归的过程。

    • 例如:涕归求n的阶乘

      long Fact ( long n ) {
          if ( n == 0) 	return 1;
          else 	return n * Fact (n-1);
      }
      
  • 以下三种情况常常用到递归方法

    • 递归定义的数学函数
    • ·具有递归特性的数据结构
    • 可递归求解的问题

20220111113423

20220111113526

20220111113604

递归问题——用分治法求解

  • 分治法: 对于一个较为复杂的问题,能够分解成几个相对简单的且解法相同或类似的子问题来求解
  • 必备的三个条件
    • 1、能将一个问题转变成、个新问题,而新问题与原问题的解法相同或类同,不同的仅是处理的对象,且这些处理对象是变化有规律的
    • 2、可以通过上述转化而使白题简化
    • 3、必须有一个明确的递归出口,或称递归的边界

20220111113924

20220111113955

20220111114105

20220111114345

20220111114553

20220111114820

递归的优缺点

20220111114943

20220111115051

20220111115104

20220111115134

20220111115151

20220111115504

3.5队列的表示和实现

20220111120727

3.3.1抽象数据类型定义

20220111121047

【算法3.0】清空

//将队列清空
Status ClearQueue(SqQueue *Q){
	Q->front = Q->rear =0;
	return OK;
}

【算法3.1】销毁

//销毁队列
Status DestroyQueue(LinkQueue *Q){
	while(Q->front){
		Q->rear=Q->front->next;//从队头开始销毁
		free(Q->front);
		Q->front = Q->rear;
	}
	return OK;
}

【算法3.2】队列是否空

//队列是否空
Status QueueEmpty(LinkQueue Q){
	if(Q.front == Q.rear)
		return TRUE;
	else
		return FALSE;
}

【算法3.3】遍历

//遍历元素
Status QueueTraverse(LinkQueue Q){
	QueuePtr p;
	p=Q.front->next;
	while(p){
		visit(p->data);
		p=p->next;
	}
	printf("\n");
	return OK;
}

3.5.2顺序队列

  • 队列的物理存储可以用顺序存储结构,也可用链式存储结构, 队列的存储方式也分为两种,即顺序队列和链式队列。

  • 队列的顺序表示——用一维数组base[MAXQSZE]

#define MAXQSIZE 100//最大队列长度
Typedef struct {
    QElemType *base;//初始化的动态分配存储空间
    int front;		//头指针
    int rear		//尾指针
}SqQueue;

20220111131411

20220111131526

解决假上溢的方法——引入循环队列

20220111131746

20220111132036

20220111132127

20220111132319

类型定义

#define MAXQSIZE 100//最大队列长度
typedef struct {
    QElemType *base;//动态分配存储空间
    int front;		//头指针,若队列不空,指向队列头元素
    int rear;		//尾指针,若队列不空,指向队列尾元素的下一个位置
} SqQueue;

【算法3.4】初始化

Status lnitQueue (SqQueue &Q){
    Q.base =new QElemType[MAXQSIZE]		//分配数组空间
    //Qbase = (QElemType*)malloc(MAXQSIZE*sizeof(QElemType));
    if(!Q.base)exit(OVERFLOW);			//存储分配失败
    Q.front=Q.rear=O;					//头指针尾指针置为0,队列为空
    return OK;
}

【算法3.5】求队长

int QueueLength (SqQueue Q){
    return (Q.rear-Q.front+MAXQSIZE)% MAXQSIZE );
}

20220111133516

【算法3.6】入队

Status EnQueue(SqQueue &Q, QElemTypee){
    if((Q.rear+1)%MAXQSIZE==Q.front)	return ERROR;		//队满
    Q.base[Q.rear]=e;				//新元素加入队尾
    Q.rear=(Qrear+1)%MAXQSIZE;	    //队尾指针+1
    return OK;
}

【算法3.7】出队

Status DeQueue (SqQueue &Q,QElemType &e){
    if(Q.front==Qrear)	 return ERROR;	//队空
    Q.base[Q.front];						//保存队头元素
    Q.front=(Q.front+1)%MAXQSIZE;		//队头指针+1
    return OK;
}

【算法3.8】取队头元素

SElemType GetHead(SqQuere Q){
    if(Q.front!=Q.rear)			//队列不为空
    	return Q.base[Q.front];	//返回队头指针元素的值,队头指针不变
}

3.5.3链式队列

20220111134657

#define MAXQSIZE 100	//最大队列长度
typedef struct Qnode {
    QElemType data;
    struct Qnode *next;
}QNode,*QuenePtr;
typedef struct {
    QuenePtr front;//队头指针
    QuenePtr rear;//队尾指针
}LinkQueue;

20220111135210

【算法3.4】初始化

20220111140055

Status InitQueue (LinkQueue &Q){
    Q.front=Q.rear=(QueuePtr) malloc(sizeof(QNode));
    if(!Q.front) 	exit(OVERFLOW);
    Q.front->next=NULL;
    return OK;
}

【算法补充】销毁

20220111140145

Status DestroyQueue (LinkQueue &Q){
    while(Q.front){
        p=Q.front->next;	//Q.rear=Q.front->next; 
        free(Q.front);		//free(Q.front);
        Q.front=p;			//Q.front=Q.rear;
    }
    return OK;
}

【算法3.6】入队

20220111140622

Status EnQueue(LinkQueue &Q, QElemType e){
    p=(QueuePtr)malloc(sizeof(QNode));
    if(!p) 	exit(OVERFLOW);
    p->data=e;	 p->next=NULL;
    Q.rear->next=p;
    Q.rear=p;
    return OK;
}

【算法3.7】出队

20220111140820

Status DeQueue (LinkQueue &Q,QElemType &e){
    if(Q.front==Q.rear)		 return ERROR;
    p=Q.front->next;
    e=p->data;
    Q.front->next = p->next;
    if(Q.rear==p)	Q.rear=Q.front;	
    delete p;
    return OK;
}

【算法3.8】取队头元素

Status GetHead (LinkQueue Q, QElemType &e){
    if(Q.front==Q.rear) 	return ERROR;
	e=Q.front->next->data;
	return OK;
}

3.6 案例分析与实现

案例 3.1: 数制的转换

[案例分析】
当将一个十进制整数 N 转换为八进制数时,在计算过程中,把 N 与 8 求余得到的八进制数的各位依次进栈,计算完毕后将栈中的八进制数依次出栈 输出,输出结果就是待求得的八进制数。
【案例实现】
在具体实现时,栈可以采用顺序存储表示也可以采用链式存储表示。
【算法步骤】

  1. 初始化一个空栈S。
  2. 当十进制数N非零时, 循环执行以下操作:
    • 把 N 与 8 求余得到的八进制数压入栈 S;
    • N更新为N与8的商。
  3. 当栈S非空时,循环执行以下操作:
    • 弹出栈顶元素e;
    • 输出 e。

[算法描述】

void conversion(int N) {	//对于任意一个非负十进制数,打印输出与其等值的八进制数
    InitStack(S); 			//初始化空栈s
    while (N) {				//当N非零时,循环
        Push(S,N%8); 		//把N与8求余得到的八进制数压入栈s
    	N=N/8; 				//N 更新为 N 与 8 的商
    }
    while (!StackEmpty (S)) {	 //当栈S非空时,循环
    	Pop (S, e); 			 //弹出栈顶元素e
    	cout<<e; 				 //输出e
    }
}

【算法分析】

​ 显然, 该算法的时间和空间复杂度均为 O(log8n)。
​ 这是利用栈的后进先出特性的最简单的例子。在这个例子中,栈的操作是单调的, 即先一味地入栈,然后一味地出栈。 也许,有的读者会提出疑问:用数组直接实现不是更简单吗?但仔细分析上述算法不难看出,栈的引入简化了程序设计的问题, 划分了不同的关注层次, 使思考范围缩小了。 而用数组不仅掩盖了间题的本质,还要分散精力去考虑数组下标增减等细节问题。

​ 在实际利用栈的问题中, 入栈和出栈操作大都不是单调的, 而是交错进行的。

案例 3.2: 括号匹配的检验

【案例分析】
检验算法借助一个栈, 每当读入一个左括号, 则直接入栈,等待相匹配的同类右括号;每当读入一个右括号,若与当前栈顶的左括号类型相同, 则二者匹配,将栈顶的左括号出栈,直到表达式扫描完毕。
在处理过程中,还要考虑括号不匹配出错的情况。 例如, 当出现(( )[ ]))这种情况时, 由于前面入栈的左括号均已和后面出现的右括号相匹配, 栈已空,因此最后扫描的右括号不能得到匹配;出现[([ ])这种错误,当表达式扫描结束时,栈中还有一个左括号没有匹配;出现(()] 这种错误显然是栈顶的左括号和最后的右括号不匹配。
【算法步骤】

  1. 初始化一个空栈S。
  2. 设置一标记性变量 flag, 用来标记匹配结果以控制循环及返回结果, l 表示正确匹配, 0表示错误匹配, flag 初值为 1。
  3. 扫描表达式,依次读入字符ch, 如果表达式没有扫描完毕或 flag 非零, 则循环执行以下操作:
    • 若 ch是左括号"[" 或"("' 则将其压入栈;
    • 若 ch是右括号")"' 则根据当前栈顶元素的值分情况考虑:若栈非空且栈顶元素是 "(",则正确匹配, 否则错误匹配, flag 置为 O;
    • 若 ch是右括号"]"' 则根据当前栈顶元素的值分情况考虑:若栈非空且栈顶元素是"["'则正确匹配, 否则错误匹配, flag 置为 0。
  4. 退出循环后, 如果栈空且 flag 值为 1' 则匹配成功, 返回 true, 否则返回 false。

【算法描述]

Status Matching(){//检验表达式中所含括号是否正确匹配,如果匹配,则返回true, 否则返回false
//表达式以"#"结束
    InitStack(S); 		//初始化空栈
    flag=1; 		    //标记匹配结果以控制循环及返回结果
    cin>>ch; 			//读入第一个字符
    while(ch!='#'&&flag) {	    //假设表达式以"#"结尾
        switch(ch) {
            case '['||'(': 	//若是左括号,则将其压入栈
                Push(S,ch); 
                break; 
            case ')': 			//若是")",则根据当前栈顶元素的值分情况考虑
            	if (! StackErnpty (S) &&GetTop (S) =='(') 
            		Pop(S,x); 	//若栈非空且栈顶元素是.. ("'则正确匹配
            	else flag=O; 	//若栈空或栈顶元素不是..("'则错误 失败
            	break; 
            case ']': 			//若是"]",则根据当前栈顶元素的值分情况考虑
            	if (! StackErnpty (S) &&GetTop (S) =='[') 
           			Pop (S, x); //若栈非空且栈顶元素是"[",则正确匹配
            	else flag=O; 	//若栈空或栈顶元素不是"[",则错误匹配
            	break; 			
        }//switch
        cin>>ch; 				//继续读入下一个字符
    }//while        
    if(StackEmpty(S)&&flag) return true; 	//匹配成功
    else return false; 						//匹配失败
}

【算法分析】
此算法从头到尾扫描表达式中每个字符, 若表达式的字符串长度为 n, 则此算法的时间复杂度为 O(n)。算法在运行时所占用的辅助空间主要取决于 S 栈的大小, 显然, S 栈的空间大小不会超过n, 所以此算法的空间复杂度也同样为 O(n)。

案例3.3: 表达式求值

【案例 分析】
任何一个表达式都是由操作数(operand·)、运算符(operator)和界限符(delimiter)组成的,
统称它们为单词。

  • 一般地,操作数既可以是常数,也可以是被说明为变量或常量的标识符;
  • 运算符可以分为算术运算符、关系运算符和逻辑运算符 3 类;
  • 基本界限符 有左右括号和表达式结束符等。

​ 为了叙述的简洁,在此仅讨论简单算术表达式的求值问题,这种表达式只含加、减、乘、除4种运算符。读者不难将它推广到更一般的表达式上。
下面把运算符界限符统称为算符
​ 我们知道,算术四则运算遵循以下 3条规则:
​ (1)先乘除,后加减;
​ (2)从左算到右;
​ (3)先括号内,后括号外。
​ 根据上述 3条运算规则,在运算的每一步中,任意两个相继出现的算符θ1和θ2之间的优先关系,至多是下面 3 种关系之一 :
​ θ1< θ2 θ1的优先权低于θ2
​ θ1 = θ2 θ1的优先权等于θ2
​ θ1 > θ2 θ1的优先权高于θ2
​ 表 3.1 定义了算符之间的这种优先关系。

20220119195254

​ 由规则(1), 先进行乘除运算,后进行加减运算,

所以有 "+" < ""; "+" < "!"; "" >"+"; "!" > "+" 等。
由规则(2), 运算遵循左结合性,当两个运算符相同时,先出现的运算符优先级高,

所以有"+" > "+"; "-" > "-"; "" > ""; "!" > "!"。
由规则(3), 括号内的优先级高,+、-、*和/为θ1 时的优先性均低于 (" "但高于 " )"。
表中的 "(" = ")" 表示当左右括号相遇时,括号内的运算已经完成。为了便于实现,假设每个表达式均以"#"开始,以"#" 结束。所以"#" = "#" 表示整个表达式求值完毕。")"与 "("、"#”与")" 以及"("与"#" 之间无优先关系,这是因为表达式中不允许它们相继出现,一旦遇到这种情况,则可以认为出现了语法错误。
【案例实现】
为实现算符优先算法,可以使用两个工作栈,一个称做OPTR,用以寄存运算符;另一个称
OPND, 用以寄存操作数或运算结果。

【算法步骤】

  1. 初始化OPTR栈和OPND栈,将表达式起始符"#"压入OPTR栈。

  2. 扫描表达式,读入第一个字符ch, 如果表达式没有扫描完毕至"#"或OPTR的栈顶元素
    不为"#"时,则循环执行以下操作:

    • 若ch不 是运算符,则压入OPND栈,读入下一字符ch;
    • 若ch是运算符,则根据OPTR的栈顶元素和ch的优先级比较结果,做不同的处理:
      • 若是小于,则ch压入OPTR栈,读入下一字符ch;
      • 若是大于,则弹出OPTR栈顶的运算符,从OPND栈弹出两个数, 进行相应运算,结果压入OPND栈;
      • 若是等于,则OPTR的栈顶元素是 "(" 且ch是")"'这时弹出OPTR栈顶的 "(",相当于括号匹配成功,然后读入下一字符ch。
  3. OPND栈顶元素即为表达式求值结果,返回此元素。

【算法描述】

char EvaluateExpression () 
{//算术表达式求值的算符优先算法,设OPTR和OPND分别为运算符栈和操作数栈
    InitStack(OPND); //初始化OPND栈
    InitStack(OPTR); //初始化OPTR栈
    Push (OPTR,'#'); //将表达式起始符"#" 压人OPTR栈
    cin>>ch; 
    while(ch!='#'||GetTop(OPTR) !='#'){ //表达式没有扫描完毕或OPTR的栈顶元素不为"# "
        if (!In (ch)) {			//ch不是运算符则进OPND栈
            Push (OPND, ch);
            cin>>ch;
        } 
        else 
        	switch (Precede (GetTop(OPTR) , ch)){ //比较OPTR的栈顶元素和ch的 优先级
                case'<': 
                    Push(OPTR,ch);cin>>ch; 	//当前字符ch压入OPTR栈,读入下一字符ch
                    break; 
                case'>': 
                    Pop(OPTR,theta); 		  //弹出OPTR栈顶的运算符
                    Pop(OPND,b);Pop(OPND,a);  //弹出OPND栈顶的两个运算数
                	Push (OPND, Operate (a, theta, b·)); //将运算结果压入OPND栈
                	break; 
                case'=': 					  //OPTR的栈顶元素是"("且ch是")"
                	Pop(OPTR,x) ;cin>>ch; 	  //弹出OPTR栈顶的"(", 读入下一字符ch
                	break; 
            }//switch
    }//while 
    return GetTop (OPND) ; 				   	  //OPND栈顶元素即为表达式求值结果
}    

函数In是判定读入的字符ch是否为运算符,Precede 是判定运算符栈的栈顶元素与读入的运算符之间优先关系的函数,Operate为进行二元运算的函数。
算法中的操作数只能是一位数,因为这里使用的OPND栈是字符栈,如果要进行多位数的运算,则需要将OPND栈改为数栈,读入的数字字符拼成数之后再入栈。
【算法分析】
​ 此算法从头到尾扫描表达式中每个字符,若表达式的字符串长度为 n, 则此算法的时间复杂度为O(n)。算法在运行时所占用的辅助空间主要取决于OPTR栈和OPND栈的大小,显然,它们的空间大小之和不会超过n, 所以此算法的空间复杂度也同样为O(n)。
【例3.2】 算法表达式的求值过程。
​ 利用算法 3.22 对算术表达式 3*(7-2)进行求值,给出其求值的具体过程。
​ 在表达式两端先增加’“#”改写为 #3*(7-2)#
具体操作过程如表 3.2 所示。
20220119202631
​ 在高级语言的编译处理过程中,实际上不只是表达式求值可以借助栈来实现,高级语言中一般语法成分的分析都可以借助栈来实现,在编译原理后续课程中会涉及栈在语法、语义等分析算法中的应用。

案例3.4: 舞伴问题

【案例分析】
对于舞伴配对问题,先入队的男士或女士先出队配成舞伴,因此设置两个队列分别存放男士
和女士入队者。假设男士和女士的记录存放在一个数组中作为输入,然后依次扫描该数组的各元
素,并根据性别来决定是进入男队还是女队。 当这两个队列构造完成之后,依次将两队当前的队
头元素出队来配成舞伴,直至某队列变空为止。 此时,若某队仍有等待配对者,则输出此队列中
排在队头的等待者的姓名,此人将是下一轮舞曲开始时第一个可获得舞伴的人。
【案例实现】
算法中有关数据结构的定义如下:

//----- 跳舞者个人信息--- -
typedef struct{ 
    char name [20]; //姓名
    char sex; //性别,IF'表示女性,'M'表示男性
}Person; 
// ----- 队列的顺序存储结构--- - -
#define MAXQSIZE 100 //队列可能达到的最大长度
typedef struct {
    Person *base; //队列中数据元素类型为Person
    int front; 	  //头指针
    int rear; 	  //尾指针
}SqQueue; 
SqQueue Mdancers,Fdancers; //分别存放男士和女士人队者队列

【算法步骤】

  1. 初始化 Mdancers 队列和 Fdancers 队列。
  2. 反复循环, 依次将跳舞者根据其性别插入 Mdancers 队列或 Fdancers 队列。
  3. 当 Mdancers 队列和 Fdancers 队列均为非空时, 反复循环, 依次输出男女舞伴的姓名。
  4. 如果 Mdancers 队列为空而 Fdancers 队列非空, 则输出 Fdancers 队列的队头女士的姓名。
  5. 如果 Fdancers 队列为空而 Mdancers 队列非空, 则输出 Mdancers 队列的队头男士的姓名。

【算法描述】

void DancePartner(Person dancer[],int num) 
{//结构数组dancer中存放跳舞的男女,num是跳舞的人数。
    Init tQueue (Mdancers) ;  //男士队列初始化
    InitQueue(Fdancers); 	  //女士队列初始化
    for(i=O;i<num;i++){ 	  //依次将跳舞者根据其性别人队
        p=dancer[i]; 		
        if (p.sex=='F')  EnQueue (Fdancers, p); //插入女队
        else EnQueue(Mdancers,p); 				//插人男队
    }
    cout<<"The dancing partners are:\n"; 
    while(!QueueEmpty(Fdancers)&&!QueueEmpty(Mdancers)) {//依次输出男女舞伴的姓名
        DeQueue(Fdancers,p); 	//女士出队
        cout<<p.name<<" "; 	//输出出队女士姓名
        DeQueue(Mdancers,p); 	//男士出队
        cout<<p.name<<endl; 	//输出出队男士姓名
    }
    if (!QueueEmpty(Fdancers)){	//女士队列非空,输出队头女士的姓名
        p=GetHead(Fdancers) 			//取女士队头
    	cout<<"The first woman to get a partner is: "<< p.name<<endl; 
    }
    else if (!QueueEmpty (Mdancers)){ 	//男士队列非空,输出队头男士的姓名
        p=GetHead(Mdancers) //取男士队头
        cout<<"The first man to get a partner is: "<< p.name<<endl; 
    }
}

【算法分析】
若跳舞者人数总计为n, 则此算法的时间复杂度为O(n)。空间复杂度取决于Mdancers队列和Fdancers队列的长度,二者长度之和不会超过n, 因此空间复杂度也同样为O(n)。

​ 队列在程序设计中也有很多应用, 凡是符合先进先出原则的数学模型, 都可以用队列。最典型的例子是操作系统中用来解决主机与外设之间速度不匹配问题或多个用户引起的资源竞争问题。

​ 例如,一个局域网上有一台共享的网络打印机,网上每个用户都可以将数据发送给网络打印机进行打印。为了保证能够正常打印,操作系统为网络打印机生成一个“作业队列",每个申请打印的 ”作业” 应按先后的顺序排队,打印机从作业队列中逐个提取作业进行打印。

​ 这方面的例子很多, 在操作系统等后续课程中会涉及大量队列这种数据结构的应用。
​ 在实际应用中,队列应用的例子更是常见,通常用以模拟排队情景。例如,拿汽车加油站说通常的结构基本上是:入口和出口为单行道,加油车道可能有若干条。每辆车加油都要经过三段路程第一段是在入口处排队等候进入加油车道;第二段是在加油车道排队等候加油;第三段是在进入出口处排队等候离开。实际上,这三段都是队列结构。若用算法模拟这个过程,总共需要设置的队列个数应该为加油车道数加上2。

posted @ 2022-01-28 14:08  肖雄_greek  阅读(150)  评论(0)    收藏  举报