栈和队列
1.栈
(1)定义
只允许在一端进行插入或删除操作的线性表。
特点:后进先出 Last In First Out
n个不同元素进栈,出栈元素不同排列的个数为 1 n + 1 C 2 n n \frac{1}{n + 1}C_{2n}^{n} n+11C2nn(卡特兰数)。
(2)顺序栈的定义和基本操作
定义
#define MaxSize 10
typedef struct
{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top;
}SqStack;
初始化栈时,将初始化栈顶指针赋为-1。
进栈
bool Push(SqStack &S,ElemType x)
{
if(S.top==MaxSize-1) //栈满,报错
return false;
S.top=S.top+1;
S.data[S.top]=x; //新元素入栈
return true;
}
出栈
bool Pop(SqStack &S,ElemType &x)
{
if(S.top==-1) //栈空,报错
return false;
x=S.data[S.top]; //栈顶元素先出栈
S.top=S.top-1; //指针再-1
}
(3)共享栈
定义
两个栈共享同一片空间。
#define MaxSize 10
typedef struct
{
ElemType data[MaxSize];
int top0; //0号栈栈顶指针
int top1; //1号栈栈顶指针
}ShStack;
初始化栈操作
void InitStack(ShStack &s)
{
S.top0=-1;
S.top1=MaxSize;
}
栈满的条件:top0+1==top1
。
(4)栈的链式存储结构
typedef struct Linknode
{
ElemType data; //数据域
struct Linknode *next; //指针域
}*LiStack;
2.队列
(1)定义
只允许在一端进行插入,在另一端删除的线性表。
队列的特点:先进先出(FIFO)
重要术语:队头、队尾、空队列
(2)顺序存储结构
分析思路:分析front
、rear
指针的指向;确定判空、判满的方法。
初始化
#define MaxSize 10 //定义队列中元素的最大个数
typedef struct
{
ElemType data[MaxSize];
int front,rear; //队头指针和队尾指针
}SqQueue; //sequence顺序
初始化时,可将front
指向队头元素,rear
指向队尾元素的后一个位置(下一个应该插入的位置)。
void InitQueue(SqQueue &Q)
{
//初始时,队头、队尾指针指向0
Q.rear=Q.front=0;
}
判断队列是否为空,可用Q.rear==Q.front
来判断。
入队
只能从队尾入队(插入)。(循环队列)
![image-20220116213719080](数据结构学习笔记.assets/image-20220116213719080.png)
bool EnQueue(SqQueue &Q,ElemType x)
{
if()
return false;
Q.data[Q.rear]=x;
Q.rear=(Q.rear+1)%MaxSize; //队尾指针+1取模
return true;
}
出队
//删除一个队头元素,并用x返回
bool DeQueue(SqQueue &Q,ElemType &x)
{
if(Q.rear==Q.front) //判断队空
return false;
x=Q.data[Q.front];
Q.front=(Q.front+1)%MaxSize;
return true;
}
获得队头元素的值的操作:
//获得队头元素的值,用x返回
bool GetHead(SqQueue &Q,ElemType &x)
{
if(Q.rear==Q.front)
return false; //队空则报错
x=Q.data[Q.front];
return true;
}
队列已满的条件:队尾指针的再下一个位置是队头,即(Q.rear+1)%MaxSize==Q.front
。
队列的元素个数=(rear-front+MaxSize)%MaxSize
。
对于判空、判满的方法,另有下列2个方案:
方案2:引入一个变量int size
作为保存队列的当前长度,当size==MaxSize
时,队列满。
方案3:判断队列已满/已空
每次删除操作成功时,都令tag=0
;每次插入操作成功时,都令tag=1
。
逻辑:只有删除操作,才可能导致队空;只有插入操作,才可能导致队满。
队空条件:front==rear&&tag==0
。
队满条件:front==rear&&tag==1
。
(3)链式存储结构
typedef struct LinkNode
{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct
{
LinkNode *front,*rear; //队列的队头和队尾指针
}LinkQueue;
入队
新元素连接到的是队列尾部。
带头结点:
void EnQueue(LinkQueue &Q,ElemType x)
{
LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode));
s->data=x;
s->next=NULL;
Q.rear->next=s; //新结点插入到rear之后
Q.rear=s; //修改表尾指针
}
不带头结点:
void EnQueue(LinkQueue &Q,ElemType x)
{
LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode));
s->data=x;
s->next=NULL;
if(Q.front==NULL) //修改front和rear的指向
{
Q.front=s;
Q.rear=s;
}
else
{
Q.rear->next=s;
Q.rear=s;
}
}
出队
带头结点:
bool DeQueue(LinkQueue &Q, ElemType &x)
{
if (Q.front == Q.rear)
return false;
LinkNode *p = Q.front->next; //用变量x返回队头元素
x = p->data;
Q.front->next = p->next; //修改头结点的next指针
if (Q.rear == p) //本次是最后一个结点出队
Q.rear = Q.front;
free(p);
return true;
}
不带头结点:则LinkNode *p=Q.front;
,即p
是队列的队头。
3.栈的应用
(1)栈在括号匹配中的应用
原则
遇到左括号就入栈;遇到右括号,就“消耗”一个左括号。当发现当前扫描到的右括号与栈顶左括号不匹配,则本次扫描失败。
算法实现
需要用到的函数如下:
//初始化栈
void InitStack(SqStack &S);
//判断栈是否为空
bool StackEmpty(SqStack S);
//新元素入栈
bool Push(SqStack &S, char x);
//栈顶元素出栈,用x返回
bool Pop(SqStack &S, char &x);
代码如下:
#define MaxSize 10
typedef struct
{
char data[MaxSize];
int top;
}SqStack;
bool bracketCheck(char str[], int length)
{
SqStack S;
InitStack(S); //初始化一个栈
for (int i = 0;i < length;i++)
{
if (str[i] == '(' || str[i] == '[' || str[i] == '{')
Push(S, str[i]); //扫描到左括号,入栈
else
{
if (StackEmpty(S)) //扫描到右括号,且当前栈空
return false;
char topElem;
Pop(S, topElem); //栈顶元素出栈
if (str[i] == ')'&&topElem != '(')
return false;
if (str[i] == ']'&&topElem != '[')
return false;
if (str[i] == '}'&&topElem != '{')
return false;
}
}
return StackEmpty(S); //检索完全部括号后,栈空则说明匹配成功
}
(2)栈在表达式求值中的应用
一些表达式的定义
波兰表达式(前缀表达式)、逆波兰表达式(后缀表达式)。
前缀表达式:运算符在两个操作数前面,如+ab
,-+abc
,-+ab*cd
;
中缀表达式:运算符在两个操作数之间,如a+b
,a+b-c
,a+b-c*d
;
后缀表达式:运算符在两个操作数后面,如ab+
,ab+c-
(或abc-+
),ab+cd*-
。
中缀表达式转后缀表达式
手算
(1)确定中缀表达式中各个运算符的运算顺序;
(2)选择下一个运算符,按照【左操作数 右操作数 运算符】的方式组合成一个新的操作数;
(3)如果还有运算符没被处理,就继续步骤2。
【注】”左优先”原则:只要左边的运算符能先计算,就优先算左边的。(可保证运算顺序唯一)
机算
从左到右出理各个元素,直到末尾。可能遇到以下三种情况:
(1)遇到操作数:直接加入后缀表达式。
(2)遇到界限符:遇到"(“直接入栈;遇到”)“则依次弹出栈内运算符并加入后缀表达式,直到弹出”(“为止。(注:”("不加入后缀表达式)
(3)遇到运算符:依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到"("或栈空则停止。之后再把当前运算符入栈。
用栈实现表达式的计算
后缀表达式的计算
(1)从左到右扫描下一个元素,直到处理完所有元素;
(2)若扫描到操作数则压入栈,并回到(1),否则执行(3);
(3)若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到(1)。
中缀表达式的计算
初始化两个栈,操作数栈和运算符栈。
若扫描到操作数,压入操作数栈;
若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)。
(3)栈在递归中的应用
函数调用时,需要用一个栈存储调用返回地址、实参、局部变量。
4.队列的应用
树的层次遍历、图的广度优先遍历、队伍在操作系统中的应用。
队伍在操作系统中的应用:
多个进程争抢着使用有限的系统资源时,FCFS(First Come First Service,先来先服务)是一种常用的策略。
例:CPU资源的分配,一台打印机打印论文(使用缓冲区,可用“队列”组织打印数据,可缓解主机与打印机速度不匹配的问题)。