4.栈与队列
本章的目的是介绍栈和队列的逻辑结构定义及在两种存储结构上如何实现栈和队列的基本运算。要求在掌握栈和队列的特点的基础上,懂得在什么样的情况下使用栈或队列。
考核要求:
识记:栈的定义、队列的定义
掌握:栈的顺序实现、栈的链接实现、队列的顺序实现
应用:循环队的组织方法及在其上进行入队、出队运算的方法和队满、队空的条件。
4.1栈的定义
栈(stack)是限定仅在表尾进行插入和删除操作的线性表
允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构
4.2栈的抽象数据类型
ADT 栈(stack)ng
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系
Operation
InitStack(*s); 初始化操作,建立一个空栈s
DestroyStack(*s); 若栈存在,则销毁它
ClearStack(*s); 将栈清空
StackEmpty(s); 若栈为空,返回true。否则返回false
GetTop(s,*e); 若栈存在且非空,用e返回s的栈顶元素
Push(*s,e); 若栈存在,插入新元素e到栈中并成为栈顶元素
Pop(*s,*e); 删除栈中栈顶元素,并用e返回其值
StackLength(s) 返回栈s的元素个数
endADT
4.3栈的顺序存储结构及实现
栈的结构定义
typedef int SElemType; //SElemType类型根据实际情况而定,这里设为int
typedef struct
{
SElemType data[maxsize];
int top //用于栈顶指针
}Sqstack;
若现在有一个栈,stacksize是5,则普通情况,空栈和栈满情况图如下
进栈操作
//插入元素e为新的栈顶元素
Status Push(SqStack *s,SElemType e)
{
if(s->top==maxsize-1)//栈满
{
return error;
}
s->top++;//栈顶指针加1
s->data[s->top]=e;//将新插入元素赋值给栈顶空间
return ok;
}
出栈操作
//若栈不空,则删除s的栈顶元素,用e返回其值,并返回ok,否则返回error
Status Pop(SqStack *s,SElemType *e)
{
if(s->top==-1)
{
return error;
}
*e=s->data[s->top]//将要删除的栈顶元素赋值给e
s->top--;//栈顶指针减1
return ok;
}
两者没有涉及任何循环语句,因此时间复杂度为O(1).
4.4两栈共享空间
两栈共享空间结构代码
//两栈共享空间
typedef struct
{
SElemType data[maxsize];
int top1;//栈1栈顶指针
int top2;//栈2栈顶指针
}SqDoubleStack;
对于push操作,除了要插入元素值参数外,还需要判断是栈1还是栈2的栈号参数stackNumber
//插入元素e为新的栈顶元素
Status Push(SqDoubleStack *s,SElemType e,int stackNumber)
{
if(s->top+1==s->top2)//栈已满,不能再插入
return error;
if(stackNumber==1)//栈1有元素进栈
s->data[++s->top1]=e;//若栈1则先top1+1后给数组元素赋值
else if(stackNumber==2)//栈2有元素进栈
s->data[--s->top1]=e;//若栈2则先top2-1后给数组元素赋值
return ok;
}
对于pop方法,参数就是判断栈1,2的参数stackNumber
//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR
Status Pop(SqDoubleStack *S,SElemType *e,int stackNumber)
{
if (stackNumber==1)
{
if(s->top1==-1)
return ERROR;/*说明栈1已经是空栈,溢出*/
*e=S->data[s->top1--];/*将栈1的栈顶元素出栈*/
}
else if (stackNumber==2)
{
if(s->top2==MAXSIZE)
return ERROR; //说明栈2已经是空栈,滋出
*e=s->data[s->top2++];/*将栈2的栈顶元素出栈*/
}
return OK;
}
4.5栈的链式存储结构及实现
栈的链式存储结构,简称为链栈
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int count;
}LinkStack;
进栈操作
//插入元素e为新的栈顶元素
Status Push(LinkStack *s,SElemType e)
{
LinkstackPtr s=(LinkstackPtr)malloc(sizeof(stackNode));
s->data=e;
s->next=s->top;//把当前的栈顶元素赋值给新结点的直接后继,如图中①
s->top=s;/*将新的结点s赋值给栈顶指针,如图中②*/
s->count++;
return OK;
}
出栈操作
/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR*/
Status Pop(LinkStack *S,SElemType *e)
{
LinkStackPtr p:
if(stackEmpty(*s))
return ERROR
*e=S->top->data;
p=S->top;//将栈顶结点赋值给p,如图③
s->top=s->top->next;//使得栈顶指针下移一位,指向后一结点,如图④
free(p);/*释放结点p*/
s->count--;
return OK;
}
4.6 栈的作用
4.7 栈的应用(暂时略过)
1.递归
2.四则运算表达式求值
4.8队列的定义
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表
队列是一种先进先出(First In First Out)的线性表,简称FIFO
运行插入的一端称为队尾,运行删除的一端称为队头
4.9队列的抽象数据类型
ADT 队列(Queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitQueue(*Q):初始化操作,建立一个空队列Q。
DestroyQueue(*Q):若队列Q存在,则销毁它。
ClearQueue(*Q):将队列Q清空。
QueueEmpty(Q):若队列Q为空,返回true,否则返回false。
GetHead(Q,*e):若队列Q存在且非空,用e返回队列Q的队头元素。
EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
DeQueue(*Q,*e):删除队列Q中队头元素,并用e返回其值。
QueueLength(Q):返回队列Q的元素个数
endADT
4.10循环队列
队列顺序存储的不足
我们假设一个队列有n个元素,则顺序存储的队列需建立一个大于n的数组,并把队列的所有元素存储在数组的前n个单元,数组下标为0的一端即是队头。所谓的入队列操作,其实就是在队尾追加一个元素,不需要移动任何元素,因此时间复杂度为O(1)
循环队列定义
所以解决假溢出的办法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。
typedef int QElemType;//QElemType类型根据实际情况而定,这里假设为int
/*循环队列的顺序存储结构*/
typedef struct
{
QElemType data[MAXSIZE];
int front;/*指针*/
int rear;/*尾指针,若队列不空,指向队列尾元素的下一个位置*/
}SqQueue;
循环队列初始化代码
/*初始化一个空队列Q*/
Status InitQueue(sqQueue *Q)
{
Q->front=0;
Q->rear=0;
return OK;
}
循环队列求队列长度的代码
//返回q的元素个数,也就是当前队列的长度
int QueueLength(SqQueue Q)
{
return (Q.rear-Q.front+maxsize)%maxsize;
}
循环队列的入队列操作
//若队列未满,则插入元素e为Q新的队尾元素
Status EnQueue(SqQueue *Q,QElemType e)
{
if((Q->rear+1)%MAXSIZE==Q->front)/*队列满的判断*/
return ERROR;
Q->data[Q->rear]=e;/*将元素e赋值给队尾*/
Q->rear=(Q->rear+1)%MAXSIZE;/*rear指针向后移一位置,*/
/*若到最后则转到数组头部*/
return OK;
}
循环队列的出队列操作
/*若队列不空,则删除Q中队头元素,用e返回其值*/
Status DeQueue(SqQueue *Q,QElemType *e)
{
if (Q->front == Q->rear)/*队列空的判断*/
return ERROR;
*e=Q->data[Q->front];/*将队头元素赋值给e*/
Q->front=(Q->front+1)%MAXSIZE;/*front指针向后移一位置,*/
/*若到最后则转到数组头部*/
return ok;
}
单是顺序存储,若不是循环队列,算法的时间性能是不高的,但循环队列又面临着数组可能会溢出的问题,所以我们还需要研究一下不需要担心队列长度的链式存储结构。
4.11队列的链式存储结构及实现
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。为了操作上的方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点
链队列的结构
typedef int QElemType;/*QElemType类型根据实际情况而定,这里假设为int*/
typedef struct QNode/*结点结构*/
{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct/*队列的链表结构*/
{
QueuePtr front,rear;/*队头、队尾指针*/
}LinkQueue;
入队操作
就是在链表尾部插入结点
//插入元素e为Q的新的队尾元素
Status EnQueue(LinkQueue *Q,QElemType e)
{
QueuePtr s=(QueuePtr)malloc(sizeof(QNode));
if(!s)/*存储分配失败*/
exit(OVERFLOW);
s->data=e;
s->next=NULL;
Q->rear->next=s;/*把拥有元素e新结点s赋值给原队尾结点的后继,*/
//见上图中①
Q->rear=s;//把当前的s设置为队尾结点,rear指向s,见上图中②
return OK ;
}
出队操作
就是头结点的后继结点出队,将头结点的后继改为它后面的结点,若链表除头结点外只剩一个元素时,则需将rear指向头节点
//若队列不空,则删除q的队头元素,用e返回其值,并返回ok,否则返回error
Status DeQueue(LinkQueue *Q,QElemType *e)
{
QueuePtr p;
if(Q->front==Q->rear)
return ERROR;
p=Q->front->next;//将欲删除的队头结点暂存给p,见上图中①
*e=p->data;//将欲删除的队头结点的值赋值给e
Q->front->next=p->next;//将原队头结点后继p->next赋值给头结点后继
//见上图中②
if(Q->rear==p)/*若队头是队尾,则删除后将rear指向头结点,见上图中③*/
Q->rear=Q->front;
free(p);
return OK;
}
对于循环队列与链队列的比较,可以从两方面来考虑,从时间上,其实它们的基本操作都是常数时间,即都为0(1)的,不过循环队列是事先申请好空间,使用期间不释放,而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。对于空间上来说,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。所以在空间上,链队列更加灵活。
总的来说,在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。