第三章栈和队列
-
栈和队列是限定插入和删除只能在表的“端点”进行的线性表。是线性表的子集(插入和删除位置受限的线性表)
-
栈具有“先进后出”的固有特性,例如碟盘子,每次只能在最上面放盘子,而且每次只能从最上面拿盘子。
需要用“栈”算法:数据转换、括号匹配检验、行编辑程序、迷宫求解、表达式求值、八皇后问题、函数调用、递归调用实现。
- 队列具有“先进先出”的特性,例如排队,每次先排的人先走。
需要用“队列”的算法:
*脱机打印输出:按申请先后顺序依次输出
*多用户系统中,多个用户排成队,分时循环使用CPU和主存
*按用户的优先级排列成多个队,每个优先级一个队列
*实时控制系统中,信号按接收的先后顺序依次处理
*网络电文传输,按到达的时间先后顺序依次进行
1.栈:只能插入和删除最后一个
-
栈(Stack)是一个特殊的线性表,是限定仅在一端(通常是表尾)进行插入和删除操作的线性表。栈后进先出(Last In First Out)的线性表,简称"LIFO"结构。
-
表尾称栈顶Top;表头称栈底Base。
-
插入元素到栈顶的操作,称为“入栈”,也叫“压栈”(PUSH)。
-
从栈顶删除最后一个元素操作,称为“出栈”,也叫弹出(POP)。
-
栈的示意图
- 入栈操作示意图
- 出栈操作示意图
【思考题】假设有3个元素a,b,c,入栈顺序是a,b,c,则它们的出栈顺序有几种可能。
答案:cba,abc,acb,bac,bca
2.队列:只能插入队尾,删除队头
- 队列(queue)是一种先进先出(FIFO)的线性表。
- 在表一端(表尾)插入,在另一端(表头)删除。
3案例引入
- 栈:进制转换、括号匹配、表达式求值
- 队列:舞伴问题
【案例3.1】进制转换
例:把十进制数159转换成八进制数
【案例3.2】括号匹配检验
左括号入栈,右括号跟栈顶括号配对。如果能配对出栈,不能配对则语法不对。
【案例3.3】表达式求值(运算符优先级优先算法)
- 表达式组成:
*操作数:常数、变量。
*运算符:算术运算符、关系运算符、逻辑运算符
*界限符:左右括弧和表达式结束符
- 任何一个算术表达式都由操作数(常数、变量)、算术运算符(+,-,*,/)、界限符组成(使用’#'作为起始结束符)。
例如:# 3 * (7 - 2) #
- 为了实现表达式求值,需要设置两个栈:一个算符栈OPTR,寄存运算符;另一个操作数栈OPND,寄存运算数和运算结果。
- 求值时,自左而右扫描表达式的每一个字符:
当扫描到运算数,压入栈OPND。
当扫描到运算符时,若运算符比OPTR栈顶优先级高,则入栈OPTR。若运算符比OPTR栈顶优先级低,则从OPND中弹出两个运算符,从栈OPTR中弹出栈顶运算符进行运算,将运算符结果压入栈OPND。
【案例3.4】舞伴问题
男士和女士各排成一队。舞会开始时,依次从男队和女队的队头各出一个人配成舞伴。如果两队初始人数不同,则较长的那队中未匹配者等待下一轮舞曲。
显然,先入队的男士或女士先出队配成舞伴。QM表示男士队列,QF表示女士队列。
4.栈的表示和操作的实现
4.1栈的抽象数据类型
ADT Stack{
数据对象:D={ai | ai ∈ ElemSet, i=1,2……}
数据关系:R1={<ai-1,ai> | ai-1,ai∈D,i=2,……}
约定an端为栈顶,a1段位栈底。
基本操作:
InitStck(&S) 初始化操作
操作结果:构造一个空栈S
DestroyStack(&S) 销毁栈操作
初始条件:栈S已存在
操作结果:栈S被销毁
StackEmpty(S) 判定S是否为空栈
初始条件:栈S已存在
操作结果:若栈S为空,返回TRUE,否则FALSE。
StackLength(S) 求栈长度
初始条件:栈S已存在
操作结果:返回S的元素个数,即栈的长度
GetTop(S,&e) 取栈顶元素
初始条件:栈S已存在且非空
操作结果:用e返回S的栈顶元素
ClearStack(&S) 栈置空操作
初始条件:栈S已存在
操作结果:将S清为空栈
Push(&S,e)
初始条件:栈S已存在
操作结果:插入元素e为新的栈顶元素
Pop(&S,&e)
初始条件:栈S已存在且非空
操作系统:删除S的栈顶元素an,并用e返回其值
}ADT Stack
栈本身是线性表,所以栈也有顺序存储与链式存储两种实现方式(即顺序栈,链栈)。
4.2顺序栈的表示和实现
利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素。栈底一般在低地址端。
-
设top指针,指示栈顶元素在顺序栈中的位置。
-
设base指针,指示栈底元素在顺序栈中的位置。
-
为方便操作,通常top指向栈顶元素之上的地址,即放到表最后一个元素后一位。
-
用stacksize表示栈可使用最大容量。
例子1:一个顺序栈的示意图
使用数组作为顺序栈的存储方式特点:简单、方便、由于数组大小固定容易产生溢出(包括上溢、下溢)。
- 上溢(overflow):栈满时,压入元素。是一种错误,使问题的处理无法进行。
- 下溢(underflow):栈空时,弹出元素。认为是一种结束条件,问题处理结束。
顺序栈的表示
#define MAXIZE 100
typedef struct{
SElemType *base; //栈底指针
SElemType *top; //栈顶指针
int stacksize; //栈可用最大容量
}SqStack;
例子:栈的例子示意图
【算法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;
}
【算法2】栈判空
Status StackEmpty(SqStack S){
//若栈为空,返回TRUE,否则返回FALSE
if(S.top == S.base)
return TRUE;
else
return FALSE;
}
【算法3】求顺序栈长度
int StackLength(SqStack S){
return S.top - S.base;
}
【算法4】清空顺序栈
Status ClearStack(SqStack S){
if(S.base)
S.top = S.base;
return OK;
}
【算法5】销毁顺序栈
Status DestoryStack(SqStack &S){
if(S.base){
delete S.base;
S.stacksize = 0;
S.base = S.top = NULL;
}
return OK;
}
【算法6】顺序栈入栈
【算法步骤】
①判断是否栈满,若满则出错(上溢)
②元素e压入栈顶
③栈顶指针+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;
}
【算法7】顺序栈出栈
【算法步骤】
①判断栈是否为空,若空则出错(下溢)
②栈顶指针-1,获取栈顶元素e
Status Pop(SqStack &S,SElemType e){
if(S.top == S.base) //栈空
return ERROR;
e=*--S.top; //--S.top; e=*S.top;
return OK;
}
4.3链栈的表示和实现
链栈:是运算受限的单链表,只能在链表头部进行操作。
链栈定义
typedef struct StackNode{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStack;
LinkStack S;
链栈示意图:
【算法1】链栈初始化
void InitStack(LinkStack &S){
//构造一个空栈,栈顶指针置空
S = NULL;
return OK;
}
【算法2】链栈判空
Status StackEmpty(LinkStack S){
if(S==NULL)
return TRUE;
else
return FALSE;
}
【算法3】链栈的入栈
【算法步骤】
①分配入栈元素空间,用指针p指向
②将新结点数据域赋值
③将新结点压入栈顶。p->next = S;
④修改栈顶指针。S=p;
Status Push(LinkStack &S,SElemType e){
//在栈顶插入元素e
p=new StackNode; //生成新结点
p->data = e; //设置新结点数据域
p->next = S; //新结点压入栈顶
S = p; //修改栈顶指针
return OK;
}
【算法4】链栈的出栈
【算法步骤】
①判断链栈是否为空,若为空返回ERROR;
②将栈顶元素赋值给e。
③临时保存栈顶空间,用指针p指向
④修改栈顶指针,指向新栈顶元素。
⑤释放p
Status Pop(LinkStack &S,SElemType &e){
if(S=NULL) //链栈判空
return ERROR;
e = S->data; //栈顶元素赋值给e
p = S; //标记当前栈顶
S = S->next; //修改栈顶指针
delete p; //释放标记的栈顶
return OK;
}
【算法5】取栈顶元素
SElemType GetTop(LinkStack S){
if(S!=NULL)
return S->data;
}
5栈与递归
-
递归定义:若一个对象部分地包含自己,或者用它自己给自己定义,则称这个对象是递归的。
-
若一个过程直接或间接调用自己,则称这个过程为递归的过程。
例如:递归求n阶乘
long Fact(long n){
if(n == 0) //基本项
return 1;
else //归纳项
return n*Fact(n-1);
}
- 常用到递归的三种情况:
*递归定义数学函数
*具有递归特性的数据结构:二叉树、广义表
*可递归求解的问题:迷宫问题、
- 递归问题——用分治法求解
分治法:对于一个比较复杂的问题,分解成几个简单的且解法相同或类似的子问题求解。
分治法求解递归问题算法一般形式:
void p(参数表){
if(递归结束条件)
可直接求解的步骤; ——基本项
else p(较小的参数); ——归纳项
}
- 递归过程解析:以求阶乘 n!为例子
6.队列的表示和操作的实现
6.1队列的抽象数据类型
ADT Queue{
数据对象:D={ai | ai∈ElemSet,i=1,2……}
数据关系:R={<ai-1,ai> | ai-1,ai∈D,i=1,2……} //约定其中a1端为队列头,an端为队列尾。
基本操作:
InitQueue(&Q)
操作结果:构造一个空队列Q
DestroyQueue(&Q)
初始条件:队列Q已存在
操作结果:队列Q被销毁
……
}ADT Queue
6.2顺序队的表示和实现
顺序队——用顺序存储方式存储队列。
用一维数组base来表示顺序队:
#define MAXQSIZE 100 //定义一个常量作为最大队列长度
typedef struct{
QElemType *base; //初始化动态分配存储空间
int front; //定义头指针
int rear; //定义尾指针
}SqQueue;
顺序表入队出队示意图:
-
当rear=MAXQSIZE时,再入队发生溢出。
*用顺序队时,如果还未有元素出队时,发生溢出时候为真溢出。
*在已经有元素出队后,发生溢出的时候为假溢出。这个时候有空出来的位置。
- 解决假溢出——引入循环队列
将base[0]接在base[MAXQSIZE -1]之后,若rear + 1 = MAXQSIZE,令rear = 0。
实现方法:利用模运算(mod,C语言中: %)。模运算即求整除的余数。
插入元素:Q.base[Q.rear] = x; Q.rear=(Q.rear+1) %MAXQSIZE;
删除元素:x=Q.base[s.front]; Q.front=(Q.front+1) %MAXQSIZE;
循环队列:循环使用为队列分配的存储空间。
循环队列解决队满时判断方法——少用一个元素空间
【算法1】队列初始化
Status InitQueue(SqQueue &Q){
Q.base = new QElemType[MAXQSIZE]; //Q.base = (QElemType*) malloc (MAXQSIZE*sizeof(QElemType));
if(!Q.base) //分配内存失败
exit(OVERFLOW);
Q.front=Q.rear=0;
return OK;
}
【算法2】循环队列——求队列长度
int QueueLength(SqQueue Q){
return ((Q.rear-Q.front+MAXQSIZE) % MAXQSIZE);
}
【算法3】循环队列——入队
Status EnQueue(SqQueue &Q,QElemType e){
if((Q.rear+1)%MAXQSIEZE == Q.front) //队满(少用一个元素空间)
return ERROR;
Q.base[Q.rear] = e; //新元素加入队尾
Q.rear=(Q.rear + 1)%MAXQSIZE; //队尾指针+1
return OK;
}
【算法4】循环队列——出队
Status DeQueue(SqQueue &Q,QElemType &e){
if(Q.front==Q.rear) //队空
return ERROR;
e=Q.base[Q.front];//保存队头元素
Q.front = (Q.front +1)%MAXQSIZE; //队头指针+1
return OK;
}
【算法5】循环队列——取队头元素
SElemType GetHead(SqQueue Q){
if(Q.front != Q.rear) //队列不为空
return Q.base[Q.front];//返回队头指针元素,队头指针不变
}
6.3链队的表示和实现
若用户无法估计所用队列的长度,则适宜采用链队列。
链队列定义
#define MAXQSIZE 100 //最大队列长度
typedef struct Qnode{
QElemType data;
struct Qnode *next;
}QNode,*QuenePtr;
typedef struct{
QuenePtr front;//队头指针
QuenePtr rear; //队尾指针
}LinkQueue;
链队列运算指针变化状况
【算法1】链队初始化
Status InitQueue(LinkQueue &Q){
Q.front=Q.rear=(QueuePtr)malloc(sizeof(QNode));
if(!Q.front)
exit(OVERFLOW);
Q.front->next=NULL;
return OK;
}
【算法2】链队列销毁
Status DestroyQueue(LinkQueue &Q){
while(Q.front){
p=Q.front->next;
free(Q.front);
Q.front=p;
}
return OK;
}
【算法3】链队入队
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;
}
【算法4】链队出队
Status DeQueue(LinkeQueue &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;
}
【算法5】链队列头元素
Status GetHead(LinkQueue Q,QElemType &e){
if(Q.front == Q.rear)
return ERROR;
e=Q.front->next->data;
return OK;
}
(QueuePtr)malloc(sizeof(QNode));
if(!p)
exit(OVERFLOW);
p->data=e;
p->next=NULL;
Q.rear->next=p;
Q.rear=p;
return OK;
}
【算法4】链队出队
```c
Status DeQueue(LinkeQueue &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;
}
【算法5】链队列头元素
Status GetHead(LinkQueue Q,QElemType &e){
if(Q.front == Q.rear)
return ERROR;
e=Q.front->next->data;
return OK;
}
学习视频:数据结构——王卓;
参考文献:数据机构C语言版第2班——严蔚敏