数据结构笔记
三、栈与队列
1. 栈
1.1 基本概念与基本操作
1.1.1 基本概念与性质
- 栈(stack):只允许在一端进行插入删除的操作的线性表。
- 栈顶(top):线性表允许进行插入删除的那一端。
- 栈底(bottom):固定,不允许进行插入删除的另一端。
- 空栈:不含任何元素的空表。
栈的操作特性:后进先出(last in first out,LIFO)
栈的数学性质:n个不同元素进栈,出栈元素不同排序的个数为 1 n + 1 C 2 n n {{1} \over {n+1}}C_{2n}^n n+11C2nn (卡特兰数Catalan)。
1.1.2 基本操作
- InitStack(&S); 初始化表,构造一个空的线性表。
- DestroyStack(&S);销毁操作,销毁线性表,释放线性表,释放L所占用的内存空间。
- Push(&S,x);元素进栈,x称为新栈顶。
- Pop(&S,&x);元素出栈,用x返回栈顶元素。
- GetTop(S,&x);获得栈顶元素。
- DestroyStack(&S); 销毁栈,释放S所占用的内存空间。
1.2 栈的顺序存储结构
1.2.1 顺序栈的实现
顺序栈:采用顺序存储的栈,利用地址连续的内存空间存放自栈底到栈顶的数据元素,附设指针(top),指向当前栈顶元素的位置。
结构体定义:
#define MaxSize 10
typedef struct {
int data[MaxSize];
int top;
}SqStack;
栈顶指针:S.top,初始设置为S.top=-1。
栈顶元素:S.data[S.top]。
入栈操作:栈不满时,栈顶指针先加1,再S.data[top]=data。
出栈操作:栈非空时,先取出栈顶元素,再将top减1,x=S.data[S.top–]。
空栈条件:S.top=-1.
栈满条件:S.top==MaxSize-1。
栈长:S.top+1。
1.2.2 顺序栈的基本运算实现
初始化
void InitStack(SqStack &S){
S.top=-1;
}
入栈操作
基本操作:
1.判断栈是否已满,满则不能入栈。S.top==MaxSize-1;
2.栈顶指针后移。S.top=S.top+1;
3.插入栈顶元素。S.data[S.top]=data;
/*
*Function:Push
*Description:入栈操作
*Input:
arg1:SqStack &S 栈
arg2:int data 入栈元素
*/
bool Push(SqStack &S,int data){
if (S.top==MaxSize-1){
return false ;
}
S.top=S.top+1;
S.data[S.top]=data;
return true;
}
出栈操作
1.判断栈是否为空,如果为空,则不能出栈。
2.先出栈,指针再减1。data=S.data[S.top–];
/*
*Function:Push
*Description:出栈操作
*Input:
arg1:SqStack &S 栈
arg2:int &data 返回的出栈元素
*/
bool Pop(SqStack &S,int &data){
if (S.top==-1){
return false ;
}
data=S.data[S.top--];
return true;
}
取栈顶元素
/*
*Function:GetTop
*Description:读栈顶元素
*Input:
arg1:SqStack S
arg2:int &data 返回的出栈元素
*/
bool GetTop(SqStack S,int &data){
if (S.top==-1){
return false ;
}
data=S.data[S.top];
return true;
}
判空、打印栈操作
/*
*Function:StackPrint
*Description:打印栈操作
*Input:
arg1:SqStack S 需要打印的栈
*/
void StackPrint(SqStack S){
for (int location=0;location<=S.top;location++){
printf("栈的第%d个元素为:%d\n",location+1,S.data[location]);
}
}
/*
*Function:StackEmpty
*Description:判空操作
*Input:
arg1:SqStack S 要判空的栈
*Return:
若空,返回true。若不空,返回false。
*/
bool StackEmpty(SqStack S){
if (S.top == -1){
return true;
}else{
return false;
}
}
1.2.2 共享栈
共享栈:两个栈共享一片空间。
优点:两个栈共享空间,避免了空间的浪费,且没有增加存取的时间复杂度。
栈满的条件:top0+1==top1
#include <stdio.h>
#define MaxSize 10
typedef struct {
int data[MaxSize];
int top0;
int top1;
}ShareStack;
void InitStack(ShareStack &S){
S.top0=-1;
S.top1=MaxSize;
}
1.3 栈的链式存储结构
链栈:栈的链式存储结构。
优点:便于多个栈共享存储空间和提高效率,而且不存在空间限制(栈满上溢)的情况。
结构体定义
#include <stdio.h>
typedef struct LinkNode{
int data;
struct LinkNode *next;
} *LinkStack;
2. 队列
2.1 队列的基本概念与基本操作
队列的基本概念
- 队列(Queue)的定义:操作受限的线性表,只允许一端插入,另一端删除。
- 入队进队:插入元素。
- 出队离队:删除元素。
- 操作特点:First In First out, FIFO.
- 队头、队首:允许删除一端。
- 队尾:允许插入一端。
- 空队列:不含任何元素的空表。
队列的基本操作
2.2 队列的顺序存储结构
2.2.1 队列的顺序存储
- 顺序队列:分配连续的存储单元放置队列元素。
- front:指向队头元素。
- rear:指向队尾元素的下一个元素。
- 队空条件:Q.front == Q.rear;
结构体定义及初始化
初始状态:Q.front == Q.rear == 0
typedef struct {
int data[MaxSize];
int front;
int rear;
}SqQueue;
void InitQueue(SqQueue &Q){
Q.rear=Q.front=0;
}
- 入队操作:队不满时,先送值到队尾元素,再将队尾指针+1;
- 出队操作:队不空时,先取队头元素值,再将队头指针+1;
注意:Q.rear=MaxSize不能作为队列满的条件(假溢出)
2.2.2 循环队列
基本概念
- 循环队列:将顺序队列臆造为一个环状的空间,即把存储队列元素的表从逻辑上视为环状。
- 操作实现:取余运算。
- 队空条件:Q.front==Q.rear。
注意:由上图(a)和图(d1)可知,队空的条件和队满的条件都为Q.front==Q.rear,为了区分有以下三种区分方式。
方法一:浪费一个存储单元来区分队空还是队满
约定:队头指针的下一位置作为队满的标志。如图(d2)
- 队满条件:(Q.rear+1)%MaxSzie==Q.front;
- 队空条件:Q.front == Q.rear;
- 队列中元素的个数:(Q.rear-Q.front-MaxSize)%MaxSize;
方法二:
在结构体中附设size数据成员用于表示元素个数。
- 队满条件:Q.size==MaxSize;
- 队空条件:Q.size==0;
typedef struct {
int data[MaxSize];
int front;
int rear;
int size;
}CQueue;
方法三:
在结构体中附设tag数据成员用于最近一次的是删除操作还是插入操作(0位删除,1为插入)。
- 队满条件:Q.front==Q.rear&&Q.tag == 1;
- 队空条件:Q.front==Q.rear&&Q.tag == 0;
typedef struct {
int data[MaxSize];
int front;
int rear;
int tag;
}CQueue;
2.2.3 循环队列的操作
结构体定义以及初始化
#include <stdio.h>
#define MaxSize 50
typedef struct {
int data[MaxSize];
int front;
int rear;
}CQueue;
void InitQueue(CQueue &CQ){
CQ.rear=CQ.front=0;
}
入队
队尾指针后移:CQ.rear=(CQ.rear+1)%MaxSize;
bool EnQueue(CQueue &CQ,int data){
if ((CQ.rear+1)%MaxSize == CQ.front)
return false;
CQ.data[CQ.rear]=data;
CQ.rear=(CQ.rear+1)%MaxSize;
return true;
}
出队
队头指针后移:CQ.front=(CQ.front+1)%MaxSize;
bool DeQueue(CQueue &CQ,int &data){
if (CQ.rear==CQ.front)
return false;
data=CQ.data[CQ.front];
CQ.front=(CQ.front+1)%MaxSize;
return true;
}
打印、判空
bool QueueEmpty(CQueue &CQ){
if(CQ.front == CQ.rear){
return true;
}else{
return false;
}
}
void QueuePrint(CQueue CQ){
for (int i=0;i<(CQ.rear-CQ.front+MaxSize)%MaxSize;i++){
printf("队列第%d个元素为:%d\n",i+1,CQ.data[(CQ.front + i) % MaxSize]);
}
}
2.3 队列的链式存储结构
2.3.1 队列的链式存储
- 链队列:队列的链式存储,本质上是同时带有队头指针与队尾指针点的单链表。
- 队头指针:指向头结点。
- 队尾指针:指向队尾结点(链表最后一个结点)。
- 队列为空条件:Q.front==NULL&&Q.rear == NULL .
注意:为了插入、删除操作同一,通常采用带头结点的链式结构。
- 空队列
2.3.2 链式队列的基本操作
结构体定义及初始化
基本操作:分配头指针、尾指针的空间,因为带有头结点,所以让Q.front->next=NULL;
typedef struct LinkNode{
int data;
struct LinkNode *next;
}LinkNode;
typedef struct {
LinkNode *front,*rear;
}LinkQueue;
void InitQueue(LinkQueue &Q){
Q.front=Q.rear=(LinkNode *)malloc (sizeof(LinkNode));
Q.front->next=NULL;
}
入队操作
基本操作:
1.分配插入结点s,s的data为插入值。s->data=data;
2.因为s是尾部进入,所以s指向NULL。s->next=NULL;
3.rear指针指向s。Q.rear->next = s;
4.rear指向队尾。Q.rear=s;
void EnQueue(LinkQueue &Q,int data){
LinkNode *s=(LinkNode *)malloc (sizeof(LinkNode));
s->data=data;
s->next=NULL;
Q.rear->next = s;
Q.rear=s;
}
出队操作
基本操作:
1.分配p指向第一个结点(即出队结点)。LinkNode *p=Q.front->next;
2.获得出队结点值。data=p->data;
3.队列front越过出队结点指向后面。Q.front->next=p->next;
4.若为唯一结点出队,设置为空队。Q.rear=Q.front;
bool DeQueue(LinkQueue &Q,int &data){
if (Q.front==Q.rear){
return false;
}
LinkNode *p=Q.front->next;
data=p->data;
Q.front->next=p->next;
if (Q.rear==p){
Q.rear=Q.front;
}
free(p);
return true;
}
判空、打印
bool IsEmpty(LinkQueue Q){
if (Q.front==Q.rear)
return true;
else
return false;
}
void PrintQueue(LinkQueue Q){
LinkNode *q=Q.front;
int i = 1;
while (q->next!=NULL){
q=q->next;
printf("队列中第%d个元素的值为%d\n",i,q->data);
i++;
}
}
2.4 双端
- 双端队列:允许两端都可以进行入队和出队操作的队列。
- 输入受限的双端队列:只允许一端插入、两端删除的线性表。
- 输出受限的双端队列:只允许一端删除、两端插入的线性表。
3. 栈和队列的应用
3.1 栈在括号匹配中的应用
算法的基本思想:
- 设置一个空栈,顺序读入括号。
- 若是右括号,则或者使置于栈顶的最急迫期待得以消解,若是括号不匹配、不合法,退出程序。
- 若是左括号,则压入栈中。算法结束时栈为空,则匹配,反之不匹配。
算法流程图如下:
bool bracketCheck(SqStack S){
printf ("请输入需要检验是否匹配的括号序列,以@为结束标识:\n");
ElementType c;
ElementType str[100];
int i = 0;
while((c=getchar())!='@')
{
str[i] = c;
i = i + 1;
}
for (int j=0;j<i;j++){
if (str[j]=='('||str[j]=='['||str[j]=='{'){
Push(S,str[j]);
}if (str[j]==')'||str[j]==']'||str[j]=='}'){
if (StackEmpty(S)){
return false;
}
ElementType topElem;
Pop(S,topElem);
if (str[j]==')' && topElem!='('){
return false;
}
if (str[j]=='}' && topElem!='{'){
return false;
}
if (str[j]==']' && topElem!='['){
return false;
}
}
}
return StackEmpty(S);
}
3.2 栈在表达式求值中的应用
基本概念:
- 中缀表达式:运算符在两个操作数中间。a+b-c*d;
- 后缀表达式:运算符在两个操作数后面。ab+cd*-;
- 前缀表达式:运算符在两个操作数前面。-+ab*cd;
3.2.1 后缀表达式
中缀表达式转后缀表达式(手算)
- 确定中缀表达式中各个运算符的运算顺序。
- 选择下一个运算符,按照 [左操作数 右操作数 运算符] 的方式组成一个新的操作数。
- 如果还有没有运算符没被处理,则继续2。
ps:
1.运算顺序不唯一,对应的后缀表达式也不唯一。
2.根据算法的确定性,机算结果只能是下图中的前者。(左优先原则)
后缀表达式的计算(手算)
- 从左到右,依次确定运算符顺序。
- 按照顺序,让操作符前面两个数执行响应的运算,合并为一个操作数。
- 运算向右移动。
中缀表达式转后缀表达式(机算)
从左到右依次处理各个元素,直到末尾。可能遇到三种情况:
- 如果遇到操作数,直接加入后缀表达式。
- 如果遇到界限符,遇到"(“直接入栈;遇到”)“则依次弹出栈內的运算符并加入后缀表达式,直到弹出”(“为止。”()"不加入后缀表达式。
- 遇到运算符,依次弹出栈空优先级高于或者等于当前运算符的所有运算符,并加入后缀表达式,若碰到"("或者栈空则停止。之后再把当前运算符入栈。
处理完所有字符后,将栈中剩余的运算符依次弹出,并加入后缀表达式。
后缀表达式的计算(机算、栈实现)
- 从左向右扫描下一个元素,直到处理完所有元素。
- 扫描到操作数则入栈,并到1,否则3。
- 扫描到运算符,弹出两个栈顶元素,执行相应的运算,先弹出的为右操作数,后弹出的为左操作数。运算结果压回栈顶。回到1。
计算示例如下:
3.2.2 前缀表达式
中缀表达式转前缀表达式手算方法:
- 确定中缀表达式中各个运算符的运算顺序。
- 选择下一个运算符,按照 [运算符 左操作数 右操作数] 的方式组成一个新的操作数。
- 如果还有没有运算符没被处理,则继续2。
ps:
1.运算顺序不唯一,对应的后缀表达式也不唯一。
2.根据算法的确定性,机算结果只能是下图中的前者。(右优先原则)
前缀表达式的计算(机算、栈实现)
- 从右向左扫描下一个元素,直到处理完所有元素。
- 扫描到操作数则入栈,并到1,否则3。
- 扫描到运算符,弹出两个栈顶元素,执行相应的运算,先弹出的为左操作数,后弹出的为右操作数。运算结果压回栈顶。回到1。
3.3 栈在递归中的应用
递归定义/递归:若在一个函数、过程或者数据结构的定义中又应用了它自身,则这个函数、过程或者数据结构称为递归定义的,简称递归。
递归模型必须满足的两个条件:
- 递归表达式(递归体)
- 边界条件(递归出口)
3.4 队列在层次遍历中的应用
3.5 队列在计算机系统中的应用
First Come First Service(FCFS):先来先服务
打印数据缓冲区