栈和队列
1.栈(Stack)的基本概念
- 后进先出(LIFO)
- 只允许在一端进行插入或删除操作的线性表
- 增(进栈),删(出栈),查(获取栈顶元素)
- 常考:根据进栈顺序,判断合法的出栈顺序(选择题)
数值最大的数字后面必然是降序
2.栈的顺序存储实现
2.1 顺序栈的定义
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶指针
} SqStack;
2.2 入栈操作
bool Push(SqStack &S,ElemType x){
if(S.top == MaxSize - 1) //栈满,报错
return false;
//这两句等价 S.data[++S.top] = x;
S.top = S.top + 1; //指针先加一,移到下一个位置
S.data[S.top] = x; //新元素入栈
return true;
}
2.3 出栈操作(获取类似)
bool Pop(SqStack &S,Elemtype &x){
if(S.top == -1)
return false;
//这两句等价 x = S.data[S.top--]
x = S.data[S.top]; //栈顶元素先出栈
S.top = S.top - 1; //指针减一
return true;
}
注意观察top指针位置,top指针是指向栈顶元素(top=0)还是指向栈顶元素后面的一个位置(初始top=-1),上方实现为第二种
2.4 共享栈
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top0; //0号栈顶指针
int top1; //1号栈顶指针
} SqStack;
void InitStack(ShStack){
S.top0 = -1;
s.top1 = MaxSize;
}
共享栈栈满的条件:top0 + 1 == top1
3. 栈的链式存储实现
- 链头 = 栈顶
- 链栈的定义和单链表没有区别
typedef struct Linknode{
ElemType data; //数据域
struct Linknode *next; //指针域
}*LiStack;
栈的链式存储操作与链表相同(推荐实现方式是不带头结点的实现方式)
4. 队列的基本概念
- 先进先出(FIFO)
- 入队,出队,读取队头元素,队空(队头=队尾)
- 只允许在一端进行插入,在另一端进行删除的线性表
4.1 队列的顺序实现
#define MaxSize 10
typedef struct{
ElemType data[MaxSize]; //用静态数组存放队列元素
int front,rear; //队头指针和队尾指针
}SqQueue
4.2 循环队列入队
bool EnQueue(SqQueue &Q,ElemType x){
//队尾指针的下一个位置是队头则证明循环队列满了
if((Q.rear+1)%MaxSize == Q.front) //队满则报错
return false;
Q.data[Q.rear] = x; //新元素插入队尾
Q.rear = (Q.rear+1)%MaxSize;//队尾指针+1取模
return true;
}
用模运算将线状的存储空间逻辑上变成了“环状“
4.3 循环队列出队
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;
}
4.4 队列已满的判断
-
队列元素的个数=(rear+MaxSize-front)%MaxSize
-
当初始化是rear=front=0时:
队满:rear的下一个元素是front时
队空:rear == front
缺点:会浪费一个节点的空间
-
解决方案:
-
利用size的大小记录队中的元素个数,从而判断队满/空
-
利用tag,删除操作时tag=0,插入操作时tag=1
队满: front == rear && tag == 1
对空: front == rear && tag == 0
4.5 其它出题方法
- 队尾指针默认是指向队尾元素的下一个位置,队头指针指向队头
4.6 队列的链式实现
//链式队列的结点c
typedef struct LinkNode{
Elemtype data;
struct LinkNode *next;
}LinkNode;
//链式队列
typedef struct{
LinkNode *front,*rear; //队列的队头和队尾指针
}LinkQueue;
4.7 链式入队(带头结点)
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; //修改表尾指针
}
4.8 链式入队(不带头结点)
void EnQueue(LinkQueue &Q,ElemType x){
LinkNode *s=(LinkNode*)malloc(sizeof(LinkNode))
s->data = x;
s->next = NULL;
//不带头结点的队列,第一个元素入队时需要特别处理
if(Q.front == NULL){ //在空队列中插入第一个元素
//修改队头队尾指针
Q.front = s;
Q.rear = s;
}
Q.rear->next = s; //新节点插入到rear之后
Q.rear = s; //修改表尾指针
}
4.9 链式出队(带头结点)
bool DeQueue(LinkQueue &Q,ElemType &x){
//如果是空队列,直接返回false
if(Q.front == Q.rear)
return false;
LinkNode *p = Q.front->next;//带头结点的后面一个结点出队
x = p->data; //用变量x返回队头元素
Q.front->next = p->next; //修改头结点的next指针
//如果删除的是队尾结点,则需修改rear指针
if(Q.rear == p)
Q.rear = Q.front;
free(p); //删除操作都需要释放指针
return true;
}
4.10 链式出队(不带头结点)
bool DeQueue(LinkQueue &Q,ElemType &x){
//如果是空队列,直接返回false
if(Q.front == Q.rear)
return false;
LinkNode *p = Q.front; //当前结点出队
x = p->data; //用变量x返回队头元素
Q.front = p->next; //修改该结点的next指针
//如果删除的是队尾结点,则需修改指针
if(Q.rear == p)
Q.rear = NULL;
Q.front = NULL;
free(p); //删除操作都需要释放指针
return true;
}
链式结构存储,需要注意 带头结点和不带头结点版本
5. 双端队列
- 允许从两端插入删除的线性表
- 输入受限的双端队列,输出受限的双端队列
- 考点:输入序列1,2,3,4 判断输出序列是否合法
先按照栈的顺序判断,再从栈中不合法的找出满足双端队列的
6. 栈的应用
6.1 括号匹配问题
- 使用顺序栈实现(也可以使用链栈)
- 匹配失败:左括号单身,右括号单身,左右括号不匹配
bool bracketCheck(char str[],int length){
SqStack S;
InitStack(S); //初始化一个栈
//遇见左括号存起来
for(int i = 0; i<length ;i++){
if(str[i]=='('||str[i]=='['||str=='{'){
Push(S,str[i]); //如果是左括号则入栈
}else{ //右括号
if(StackEmpty(s)) //扫描到右括号,且当前栈空
return false;
char topElem;
Pop(S,topElem); //栈顶元素出栈,存值到topElem
if(str[i]==')' && topElem!='(')
return false;
if(str[i]==']' && topElem!='[')
return false;
if(str[i]=='}' && topElem!='{')
return false;
}
}
return StackEmpty(S); //检索完括号后,栈空说明匹配成功
}
6.2 表达式求值问题
-
三种算数表达式:中缀表达式,*后缀表达式,前缀表达式
-
*后缀表达式考点:中缀表达式转后缀表达式,后缀表达式求值
-
前缀表达式考点:中缀表达式转前缀表达式,前缀表达式求值
-
中缀表达式:操作数,运算符,界限数 a+b (平时熟悉的)
-
后缀表达式(逆波兰表达式):运算符放在操作数后面 a b +
-
前缀表达式(波兰表达式):运算符放在操作数的前面 + a b
后缀表达式应用较广,考试居多
6.2.1 中缀转后缀表达式(手算)
- 中缀转后缀
1.确定中缀表达式中各个运算符的运算顺序
2.选择下一个运算符,按照左操作数,右操作数,运算符的方式组成一个新操作数
3.如果还有运算符没有被处理,就继续第二步
左优先原则:只要左边运算符能先计算,就优先算左边
前缀表达式则为:右优先原则
-
计算方法:从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行相应的运算合体为一个操作数
-
特点:最后出现的操作数最先被运算(栈)
6.2.2 中缀转后缀表达式 (机算)
- 初始化栈,用于保存还不能确定运算顺序的运算符
- 从左到右处理各个元素,直到末尾,可能有三种情况
1.遇到操作数,直接加入后缀表达式
2.遇到界限符,遇到 ‘(’ 直接入栈,遇到 ‘)’则依次弹出栈内运算符,并加入后缀表达式,直到弹出 ‘(’ 为止,注:界限符不加入表达式
3.遇到运算符,一次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到 ‘(‘ 或栈空则停止,之后再把当前运算符入栈
4.按照以上方法处理完所有字符后,将栈中剩余元素依次弹出,并加入后缀表达式
6.2.3 后缀表达式计算(机算)
- 从左往右扫描(左优先)下一个元素,直到处理完所有元素
- 若扫描到操作数则压入栈,并回到第一步,否则执行第三步
- 若扫描到运算符,则弹出两个栈顶元素,执行相应的运算,运算结果压回栈顶,回到第一步
- 注:先出栈的是右操作数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ni8s20rE-1611910326461)(%E4%B8%AD%E7%BC%80%E8%BD%AC%E5%90%8E%E7%BC%80.jpg)]
中缀表达式类似,有部分区别
1、从右往左扫描(右优先)
2、按照运算符,左操作数,右操作数的方式
3、先出栈的是左操作数
6.2.4 中缀表达式计算
-
中缀表达式转后缀表达式 & 后缀表达式计算的结合
-
运算符栈 & 操作数栈的结合
-
具体步骤:
- 初始化两个栈,运算符栈和操作数栈
- 若扫描到操作数,压入操作数栈
- 若扫描到运算符或者界限符,按照 “中缀转后缀” 相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素,并执行相应运算,运算结果再压回操作数栈)
6.3 栈在递归中的应用
- 递归:最后调用的函数,最先执行结束(LIFO类似栈)
- 递归:递归表达式(递归体),边界条件(递归出口)
- 缺点:递归层数太多可能会导致栈溢出,可能包含很多重复操作
- 递归解决的问题:可以把原始问题转化为属性相同,但规模较小的问题
7. 队列的应用
- 树的层次遍历
每一次遍历处理队头元素,将孩子节点存到队列的队尾
- 图的广度优先遍历
当我们遍历一个结点的时候检查其它相邻的结点有没有被遍历过,每一遍历过则放在队尾,直到处理完所有结点队列为空时,广度优先遍历完成
- 在操作系统中的应用
多个线程争抢有限资源,FCFS:先来先服务