文章目录
栈的基本概念
栈的定义
栈的基本操作
栈的基本操作代码实现
//栈的基本操作
//初始化一个栈
InitStack(&S)
//销毁栈
DestroyStack(&S)
//进栈
Push(&S,x)
//出栈
Pop(&S,&x)
//读取栈顶元素
GetTop(S,&x)
//判断栈是否为空
StackEmpty(S)
栈的常考题型
栈的顺序存储实现
顺序栈的定义
初始化操作
进栈操作
出栈操作
读栈顶元素操作
另一种方式
共享栈
顺序栈存储实现代码实现
#include<stdio.h>
//顺序栈的定义
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
int data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶指针
}SqStack;
//顺序栈的初始化
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针
}
//判断顺序栈是否为空
bool StackEmpty(SqStack S){
if(S.top == -1){ //栈空
return true;
}else{ //栈不空
return false;
}
}
//新元素入栈
bool Push(SqStack &S,int x){
if(S.top == MaxSize-1){ //栈满,报错
return false;
}
S.data[++S.top] = x; //指针加1,新元素入栈
return true;
}
//出栈操作
bool Pop(SqStack &S,int &x){
if(S.top == -1){ //栈空,报错
return false;
}
x = S.data[S.top--]; //栈顶元素出栈,指针减1
return true;
}
//读取栈顶元素
bool GetTop(SqStack S,int &x){
if(S.top == -1){ //栈空,报错
return false;
}
x = S.data[S.top]; //x记录栈顶元素
return true;
}
//共享栈的定义
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
int data[MaxSize]; //静态数组存放栈中元素
int top0; //0号栈栈顶指针
int top1; //1号栈栈顶指针
}ShStack;
//共享栈的初始化操作
void InitStack(ShStack &S){
S.top0 = -1; //初始化0号栈栈顶指针
S.top1 = MaxSize; //初始化1号栈栈顶指针
}
//共享栈判断栈满
bool FullStack(ShStack S){
return S.top0 + 1 == S.top1;
}
栈的链式存储实现
链栈的定义
链栈的基本操作
- 用链式存储实现的栈它的本质上也是一个单链表
- 只不过我们规定我们只能在头结点这一端进行插入和删除操作
- 也就是把链头的这一段看成是我们栈顶的这一端
- 所以链栈的定义和单链表的定义几乎没有区别,只是把名字稍微改了下而已
链栈的代码实现
#include<stdio.h>
//链栈的定义
typedef struct LinkNode{
int data; //数据域
struct LinkNode *next; //指针域
}*LinkStack; //栈类型定义
//链栈的初始化
bool InitStack(LinkStack *L){
L->next = NULL;
return true;
}
//链栈进栈操作
bool Push(LinkStack &L,int e){
if(L == NULL){
return false;
}
LinkStack *s = (LinkStack *)malloc(sizeof(LinkStack));
if(s == NULL){
return false;
}
s->data = e;
s->next = L->next;
L = s;
return true;
}
//链栈的出栈操作
bool Pop(LinkStack &L,int &e){
if(L == NULL){
return false;
}
LinkStack *q = L;
e = q->data;
L = q->next;
free(q);
return true;
}
//链栈获取栈顶元素
bool GetTop(LinkStack L,int &x){
if(L == NULL){
return false;
}
x = L->data;
return true;
}
//链栈如何判空/判满
bool IsEmpty(LinkStack L){
if(L == NULL){
return true;
}else{
return false;
}
}
队列的基本概念
队列的定义
队列的基本操作
//队列的基本操作
//初始化队列
InitQueue(&Q)
//销毁队列
DestroyQueue(&Q)
//入队
EnQueue(&Q,x)
//出队
DeQueue(&Q,&x)
//获取队头元素
GetHead(&Q,&x)
//判断队列是否为空
IsQueueEmpty(Q)
队列的顺序实现
定义
初始化操作
入队操作
循环队列
- rear指针指向下一个可以插入数据的位置的循环队列中队列满的条件为什么会空一块出来?
- 我们是通过front指针和rear指针是否指向同一个位置来判断队列是否为空,如果不留一个空则无法判断队列此时是满还是为空,只能牺牲掉一个数据单元
入队操作
出队操作
方案一:判断队列已满/已空(牺牲一片存储空间)
方案二:判断队列已满/已空(size)
方案三:判断队列已满/已空(tag)
tag == 0
表示最近执行的是一次删除
操作tag == 1
表示最近执行的是一次插入
操作
其他出题方法
队列顺序实现代码实现
#include<stdio.h>
//顺序链表的定义
#define MaxSize 10 //定义队列中元素的最大个数
typedef struct{
int data[MaxSize]; //用静态数组存放队列元素
int front,rear; //队头指针和队尾指针
}SqQueue;
//顺序队列的初始化操作
void InitQueue(SqQueue &Q){
//初始化时,队头和队尾指针指向0
Q.front = 0;
Q.rear = 0;
}
//入队操作
bool EnQueue(SqQueue &Q,int x){
if((Q.rear + 1) % MaxSize == Q.front){ //队列满则报错
return false;
}
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1) % MaxSize;
return true;
}
//出队操作
bool DeQueue(SqQueue &Q,int &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,int &x){
if(Q.rear == Q.front){
return false;
}
x = Q.data[Q.front];
return true;
}
//判断队列是否为空
bool IsQueueEmpty(SqQueue Q){
if(Q.rear == Q.front){
return true;
}else{
return false;
}
}
队列的链式实现
定义
初始化(带头结点)
初始化(不带头结点)
入队(带头结点)
入队(不带头结点)
出队(带头结点)
出队(不带头结点)
队列满的条件
队列链式实现代码实现
#include<stdio.h>
//链队列的定义
typedef struct LinkNode{
int data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front,*rear;
}LinkQueue;
//初始化(带头结点)
void InitQueue(LinkQueue &Q){
//初始化时,front和rear指针都指向头结点
LinkQueue *s = (LinkNode *)malloc(sizeof(LinkNode));
Q.front = s;
Q.rear = s;
}
//判断队列是否为空(带头结点)
bool IsQueueEmpty(LinkQueue Q){
if(Q.front == Q.rear){
return true;
}else{
return false;
}
}
//初始化(不带头结点)
void InitQueue(LinkQueue &Q){
Q.front = NULL;
Q.rear = NULL;
}
//判断队列是否为空(不带头结点)
bool IsQueueEmpty(LinkQueue Q){
if(Q.front == NULL){
return true;
}else{
return false;
}
}
//入队(带头结点)
void EnQueue(LinkQueue &Q,int 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,int x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
if(Q.front == NULL){
Q.front = s;
Q.rear = s;
}else{
Q.rear->next = s; //新结点插入到rear之后
Q.rear = s; //修改表尾指针
}
}
//出队(带头结点)
bool DeQueue(LinkQueue &Q,int &x){
if(Q.front == Q.rear){
return false;
}
LinkNode *p = Q.front->next;
x = p->data;
Q.front->next = p->next;
if(Q.rear == p){
Q.rear == Q.front;
}
free(q);
return false;
}
//入队(不带头结点)
bool DeQueue(LinkQueue &Q,int &x){
if(Q.front == NULL){
return false;
}
LinkNode *p = Q.front;
x = p->data;
Q.front = p->next;
if(Q.rear == p){
Q.front == NULL;
Q.rear == NULL;
}
free(q);
return false;
}
双端队列
定义
考点:判断输出序列合法性
栈
输入受限的双端队列
- 栈中合法的序列,在双端队列中也一定合法
- 由于只能在一端进行输入,所以在序号较大的元素出队之前,其他的序号较小的元素已经可以确定它们在队列里面的相对位置
- 绿色的和有下划线的都是合法的,红色的无下划线的是非法的
输出受限的双端队列
- 栈中合法的序列,双端队列中也一定合法
- 我们在对这些序列进行验证的时候,很重要的一点就是如果你在输出序列当中看到了某一个序号的元素,那么在这个元素输出之前意味着它之前的所有元素肯定都已经输入到这个队列里面了
栈在括号匹配中的应用
括号匹配问题
算法演示
-
遇到左括号就入栈
-
遇到右括号,就“消耗”一个左括号
-
case_1:
- 前面3个括号都是左括号,分别压入栈中
- 遇到了一个右括号③,栈顶弹出一个左括号③和其匹配
- 遇到了一个右括号②,栈顶弹出一个左括号②和其匹配
- 遇到了一个左括号④,压入栈中
- 遇到了一个右括号④,栈顶弹出一个左括号④和其匹配
- 遇到了一个右括号①,栈顶弹出一个左括号①和其匹配
- case_2:
- 前面都是左括号,分别压入栈中
- 遇到了一个右括号③,栈顶弹出一个左括号③和其匹配
- 遇到了一个右括号②,栈顶弹出一个左括号②和其匹配
- ②括号匹配失败,本次括号匹配结束
- case_3:
- case_4:
算法实现详细过程
算法的代码实现
栈在表达式求值中的应用
大家熟悉的算术表达式
波兰数学家的灵感
中缀、后缀、前缀表达式
中缀表达式转后缀表达式(手算)
后缀表达式的计算(手算)
后缀表达式的计算(机算)
- 扫描到A、B两个操作数,直接压入栈中
- 扫描到的是+操作符,依次弹出两次栈顶元素B、A,执行A+B(注意这里先弹出的是后操作数,后弹出的是前操作数)
- 类似的,这种算法思想不仅仅可以求值,而且还可以实现后缀表达式转化为带括号的中缀表达式
中缀表达式转前缀表达式(手算)
前缀表达式的计算
中缀表达式转后缀表达式(机算)
不带界限符(括号)的例子
-
依次扫描,遇到操作数
A
直接加入后缀表达式
-
往下扫描,遇到操作符
+
,由于此时栈内为空,直接把操作符压入栈内
-
往下扫描,遇到操作数
B
直接加入后缀表达式
-
往下扫描,遇到操作符
-
,由于此时-
的优先级和+
是同一个优先级的,所以我们要先把栈里面的+
弹出栈(这里这样操作的依据是在+
后面扫面到了一个-
。这就说明+
和-
中间肯定是加进去了一个操作数,扫到-
就说明中间加入的这个数肯定要进行加法运算也要进行减法运算,而由于这两个运算的优先级是相等的,根据上面“左优先”的原则,我们要先执行左边的运算符所代表的运算,后缀表达式中各运算符出现的先后顺序就是它们生效的顺序)
-
然后把
A+B
看作一个整体继续往后扫描,C
是一个操作数可以直接输出
-
再往后是一个操作符
*
,经过检查发现此时栈顶的元素是一个-
,这意味着我们扫描到的C
这个操作数前面是个减法运算,后面是个乘法运算,根据先乘除后加减,所以就不能先把-
给弹出栈,但是我们也不能直接把*
直接加入表达式让乘法先运行,因为我们不确定后面会不会出现括号,所以我们这里先将*
给压入栈中
-
再往后扫描是一个操作数
D
,直接加入表达式中
-
往下扫描,扫描到了一个操作符
/
,意味着这个/
左边的操作数D
既要进行乘法操作也要进行除法操作,由于除法和乘法它们的优先级是相等的,所以我们可以让左边的乘法先生效,先把这个*
给弹出栈,这个时候我们需要把CD*
和AB+
都看成一个整体 -
此时栈顶元素是
-
,意味着此时/左边的操作数CD*
既要进行减法也要进行除法操作,虽然除法此时的优先级是比减法要高的,但是我们此时不能确定/
后面会不会跟着类似于(E+F)
这样的小括号,所以我们此时也不能确定此时可不可以让除法先生效,所以此时还是需要把/
给压入栈里面
-
继续往后是一个操作数
E
,可以直接输出
-
再往后是一个操作符
+
,由于此时的栈顶元素是一个/
,意味着+左边的操作数既需要除法也需要加法,没有括号,此时我们就可以确定应该让除法先生效,我们就可以大胆地把优先级更高的/
给弹出栈中,此时意味着我们需要把CD*E/
看作一个整体
-
此时的栈顶元素是
-
,意味着+
中间的这个操作数既要减法也要加法,根据优先级以及左优先,我们让-
弹出栈,加入表达式当中
-
再往后是一个操作数F,直接加入表达式,此时的字符都处理完了,我们把栈里的运算符依次弹出加入表达式
带有界限符(括号)的例子
- 接下来扫到的是一个界限符
(
,如果遇到(
我们要把它直接入栈,但是(
是不加入后缀表达式的
- 此时扫描到的是一个
-
,按照规则我们需要弹出栈顶所有优先级高于或者等于-
的运算符(遇到左括号(
或者栈空则停止) - 当前栈中
(
上面没有任何操作符,所以我们直接把-
压进栈中 - 背后的逻辑是扫描到
-
的时候,-
左边的操作数的左边有一个左括号,显然我们需要优先计算括号里面的内容,但是我们不能确定-
后面的操作数后面会不会跟有一个乘法或者除法操作,所以我们不能确定这个减法是否可以立即生效,所以需要把-
压入栈中
- 当我们遇到右括号
)
的时候,我们需要依次弹出栈内的运算符并加入后缀表达式,知道弹出左括号(
为止,但是注意左括号(
不能加入后缀表达式 - 所以我们这里弹出
-
,加入后缀表达式,再弹出左括号(
,舍弃 - 背后的逻辑是,扫到右括号
)
的时候我们已经可以知道括号的作用范围了,我们必须优先计算括号内的内容,所以括号里面的所有内容都可以先生效,所以我们可以大胆地弹出括号里面的内容
- 可以利用下面的表达式自己模拟一下过程:
中缀表达式的计算(用栈实现)
实现原理
具体实现过程
- 扫到
A
,直接放入操作数栈里面
- 扫到
+
,按照“中缀转后缀”的逻辑,此时栈是空的,直接把+
压入栈中
- 扫描到操作数
B
,把它压进操作数栈里面
- 扫描到一个
-
,按照“中缀转后缀”的逻辑,我们已经可以确定栈顶的+
已经可以生效了,所以我们需要将运算符栈里面的栈顶元素+
弹出栈 - 运算符
+
弹出后,我们需要在操作数栈里弹出两个栈顶元素A
和B
并执行运算A+B
后再压回操作数栈里 - 再把当前扫描到的
-
给压入栈里
- 扫描到的是操作数
C
,直接入栈
- 扫描到的是
*
,*
的优先级比-
高,所以我们不需要把-
弹出,直接把*
压入运算符栈中
- 接下来遇到一个操作数
D
,入栈
- 扫描到一个
/
,根据”中缀转后缀“的逻辑,我们需要把*
弹出栈,此时*
运算符生效,我们需要在操作数栈中弹出两个栈顶元素C
和D
并进行计算C*D
再把结果压回操作数栈
- 扫描到的是操作数
E
,直接入栈
- 扫描到的是
+
,根据”中缀转后缀“的逻辑,此时/
运算符生效,我们需要在操作数栈中弹出两个栈顶元素E
和C*D
并进行计算(C*D)/E
再把结果压回操作数栈 - 根据”中缀转后缀“的逻辑,此时
-
运算符生效,我们需要在操作数栈中弹出两个栈顶元素A+B
和(C*D)/E
并进行计算(A+B)-(C*D)/E
再把结果压回操作数栈 - 再把当前扫描到的运算符
+
压到运算符栈里面
- 之后扫描到的是操作数F,直接把它压入栈里
- 当我们扫描完所有的东西之后,按照”中缀转后缀“的逻辑,我们需要把运算符栈里面的所有运算符都依次地弹出栈
- 每当弹出一个运算符的时候就需要让一个运算符生效,由于此时运算符栈里面只剩下
+
,则让+生效,根据”中缀转后缀“的逻辑,我们需要在操作数栈中弹出两个栈顶元素F
和(A+B)-(C*D)/E
并进行计算(A+B)-(C*D)/E+F
再把结果压回操作数栈 - 最后留在操作数栈里面的内容就是我们所求的表达式的值
栈在递归中的应用
函数调用背后的过程
栈在递归当中的应用
队列的应用
树的层次遍历
- 在访问一个结点的时候要分别把其左右孩子依次放到队列的队尾
- 遍历完的结点可以出队并删除
图的广度优先遍历
- 新建一个队列
- 当我们遍历一个结点的时候,就需要检查和这个结点相邻的其他结点有没有被遍历过,没有被遍历过的要一次放到队列的队尾
- 访问处理完的结点就可以让其出队
队列在操作系统中的应用
特殊矩阵的压缩存储
一维数组的存储结构
二维数组的存储结构
普通矩阵的存储
对称矩阵的压缩存储
三角矩阵的压缩存储
三对角矩阵的压缩存储
- 三对角矩阵,又称为带状矩阵
稀疏矩阵的压缩存储