栈和队列
一、栈
定义:
只允许在一端进行插入或删除的操作的线性表。
操作受限的线性表
栈的操作特性:后进先出(LIFO)
栈的数学性质:
n个元素进栈,出栈元素不同排列组合的个数: 1/(n+1) * C2nn
栈的顺序存储结构
采用顺序存储的栈称为顺序栈。
利用一组地址连续的存储单元存放栈底到栈顶的数据元素,同时附设一个 指针(top) 指示栈顶元素位置。
栈的存储类型描述
与顺序表类似
#define MaxSize 50
typedef struct{
int data[MaxSise];//静态数组
int top;
}SqStack;
- 栈顶指针: S.top
- 进栈操作:
栈不满时,栈顶指针先+1,再送值到栈顶元素。 - 出栈操作:
栈不空时,先取栈顶元素,再将栈指针 -1; - (若top初始值赋值为-1)
栈空条件:S.top==-1;
栈满条件:S.top==MaxSize-1;
栈 长 : S.top+1
顺序栈的基本运算
== 创、增、删、查都是O(1)复杂度==
初始化
void InitStack(SqStack &S){//注意&符号
S.top = -1;
}
判栈空
bool StackEmpty(SqStack S){
if(top == -1){
return true;
}else{
return false;
}
}
进栈
bool Push(SqStack &S,int x){//注意&符号
if(S.top==S..MaxSize-1) return false;
S.data[++S.top] = x;
return true;
}
出栈
数据还残留再内存,逻辑上被删除。
bool Pop(SqStack &S,int &x){
if(S.top == -1) return false;
x = S.data[S.top--];
return true;
}
若top初值为1:
- 入栈:先入栈再top+1
- 出栈:先top-1再出栈
读栈顶元素
bool GetTop(SqStack &S,int &x){
if(top == -1) return false;
x = S.data[S.top];
return true;
}
共享栈
利用栈底位置相对不变性质,可让两个顺序栈共享一个一维数组空间。(两个栈从两边往中间增长)
共享栈可以节省存储空间,降低发生上溢的可能(栈顶指针超出最大范围)
#define MaxSize 10
typedef struct{
int data[MaxSize];
int top0;//0号栈栈顶指针
int top1;//1号栈栈顶指针
}ShStack;
//初始化栈
void InitStack(ShStack &S){
S.top0 = -1;
S.top1 = MaxSize;
}
栈满条件:top1=top0+1
栈的链式存储结构
采用链式存储的栈成为链栈。
优点:
- 便于多个栈共享存储空间和提高效率
- 不存在栈满上溢的情况
- 通常采用单链表实现,并规定所有操作都是在单链表的表头进行的。(推荐不带头节点的)
存储类型描述
typedef struct Linknode{
int data;
struct Linknode *next;
}*LiStack;
操作
进栈对应单链表的头插法
出栈对应单链表的后删操作
初始化
void InitStack(LinkStack &S){
S = (LinkStack)malloc(sizeof(Linknode));
S->next = NULL;
}
判空
bool isEmpty(LinkStack S){
if(S->next==NULL) return true;
return false;
}
入栈
bool Push(LinkStack &S,int e){
LinkStack p;
p=(LinkStack)malloc(sizeof(Linknode));
if(p==NULL) return false;
p->data = e;
p->next = S->next
S->next = p;
return true;
}
出栈
bool Pop(LinkStack &S,int &x){
if(S->next = NULL) return false;
LinkStack p = S->next;
x = p->data;
S->next = p->next;
free(p);
return true;
}
取栈顶元素
boll GetTop(LinkStack S,int &x){
if(S->next = NULL) return false;
x = S->next->data;
return false;
}
二、队列
定义:
也是一种操作受限的线性表;
只允许在表的一端进行插入(入队),在表的另一端进行删除(出队)。
操作特性:先进先出(FIFO)
队列的顺序实现
分配一块连续的存储单元存放队列中的元素,并附设两个指针:队头指针front,队尾指针
存储类型描述
#define MaxSize 50
typedef struct{
int data[MaxSize];
int front,rear;
}SqQueue;
- 判空:Q.front == Q.rear
- 进栈操作:队不满时,先送值到队尾元素,再将队尾指针 -1。
- 出队操作:队不空时,先取队头元素值,再将队头指针 +1.
操作
初始化
<rear和front初始化为0,表示front指向最后一个元素所在位置,rear指向下一个元素放置位置>
void InitQueue(SqQueue &Q){
Q.rear = Q.front = 0;
}
判空
bool QueueEmpty(SqQueue Q){
if(Q.rear == Q.front) return true;
else return fasle;
}
入队
bool EnQueue(SqQueue &Q,int x){
if(栈满) return false;
Q.data[Q.rear++] = x;//先添后+1
return true;
}
注意:
不能用Q.rear == MaxSize作为队满条件。
上图最后一种状态,满足Q.rear == MaxSize,出现上溢出,是一种假溢出。
循环队列(解决假溢出)
把存储队列的元素的表从逻辑上视为一个环,称循环队列
- 初始时:Q.front = Q.rear = 0;
- 队头元素进1:Q.front = (Q.front+1)%MaxSize;
- 队尾元素进1:Q.rear = (Q.rear+1)%MaxSize;
- 队列长度:(Q.rear-Q.front+MaxSize)%MaxSize
判空条件的讨论
若入队元素快于出队元素速度,rear很快就会赶上front,则对空和堆满条件都是Q.front ==Q.rear;
- 牺牲一个存储单元来区分对空和堆满(常用)
队满条件:(Q.rear+1)%MaxSize==Q.front
队空条件:Q.front==Q.rear
队中元素个数(Q.rear-Q.front+Q.MaxSize)%Q.MaxSize
- 类型中添加表示元素个数的数据成员
入队成功:size++;出队成功:size–
- 队空条件:Q.size == 0
- 队满条件:Q.size == MaxSize
队满和队空都会出现Q.front==Q.rear
- 类型中添加tag数据成员
最近进行的是删除:tag=0;最近进行的是插入:tag=1;
只有删除才会导致队空;只有插入才会导致队满。
队满条件:(Q.rear==Q.front) && (tag==1)
队空条件:(Q.rear==Q.front) && (tag==0)
循环队列操作
1.初始化
void InitQueue(SqQueue &Q){
Q.rear = Q.front=0;
}
- 判空
bool isEmpty(SqQueue Q){
if(Q.rear = Q.front) return true;
return false;
}
- 入队
bool EnQueue(SqQueue &Q,int e){
if((Q.rear+1)%MaxSize==Q.front) return false;//是否队满
Q.data[Q.rear]=e;
Q.rear = (Q.rear+1)%MaxSize;
return true;
}
- 出队
bool DeQueue(SqQueue &Q,int &x){
if(Q.rear = Q.front) return false;//判队空
Q.front = (Q.front+1)%MaxSize;
return true;
}
队列的链式存储
队列的链式表示称为链队列,实际上是一个同时带有队头指针和队尾指针的单链表。
(注意区分带头结点和不带头结点)
存储类型描述
typedef struct{//链式队列结点
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;
}
- 判空
bool IsEmpty(LinkQueue Q){
if(Q.front==Q.rear) return true;
return false;
}
- 入队
void EnQueue(LinkQueue &Q,int e){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = e;s->next = NULL;
Q.rear->next = s;
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 = p->next;
//若只剩最后一节点,rear也得变
if(Q.rear==p) Q.rear = Q.front;
free(p);
return true;
}
双端队列
- 双端队列是指允许两端都可以进行入队和出队操作的队列。
- 其元素的逻辑结构仍然是线性结构
- 将队列的两端分别称为前端和后端
输出受限的双端队列:
允许在一端进行插入和删除,但在另一端只允许插入的双端队列。
输入受限的双端队列:
允许在一端进行插入和删除,但在另一端只允许删除的双端队列。
三、栈和队列的应用
1.栈→括号匹配
最后出现的左括号最先被匹配(LIFO)
算法思想:
- 初始设置一个空栈,顺序读入括号。
- 若是右括号,则或者使置于栈顶的左括号消解(左括号出栈),或者是不合法的情况(左右括号不批配)程序结束。
- 若只左括号,压入栈中
算法结束时,栈为空,否则括号序列不匹配
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.栈→表达式求值
中缀转后缀(手算)
- 运算顺序不唯一,因此后缀表达式不唯一。
- 保证手算和计算结果相同,采用左优先原则:只要左边的原算法符能算就先算左边的。
- 采用左优先可以保证顺算顺序唯一。
中缀表达式转后缀后缀表达式(机算)
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。
从左到右处理各个元素,直到末尾。可能出现三种情况:
①、遇到操作数。直接加入后缀表达式
②、遇到界限符"(“直接入栈:遇到”)“则依次弹出站内运算符并加入后缀表达式,直到弹出“(”。“(”不加入后缀表达式
③、遇到运算符。依次弹出栈中优先级高高于或等于当前运算符的所有运算符(*/优先级高于±),并加入后缀表达式,若碰到”("或栈空则停止。之后再把运算符进栈。
按上述方法处理完后,将栈中剩余运算符依次弹出,并加入后缀表达式。
后缀中字母顺序和中缀中的一样
- 操作符加入后缀表达式
- 运算符入栈,栈中无高于等于+的运算符
- 操作数加入后缀表达式
- 运算符入栈,无比*优先级高或者相等的
- 界限符"("直接入栈
- 操作数直接加入后缀表达式
- 运算符-,将栈中优先级高于或等于-的运算符输出,碰到"("停止。-入栈。
- 操作数
- 界限符")",依次弹出栈中运算符并加入后缀表达式,直到碰到")"为止
- 运算符-,弹出栈中优先级比-高或等于的运算符
- 操作数
- 运算符/
- 操作数
- 弹出栈中剩余操作符
中最表达式的计算(用栈实现)
后缀表达式的计算(机算)
用栈实现后缀表达式的运算
①、从左往右扫描下一个元素,直到处理完所有元素
②、若扫描到操作数则压入栈,并回到①;否则执行③
③、若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①
注意:
- 先出栈的时右操作数。
- 若表达合法,则栈中只会留下最后一个元素(最终结果)。
中缀表达式转前缀表达式(手算)
采用**右优先原则**:只要右边的运算符能先计算,就优先算右边的。
前缀表达式的计算(机算)
用栈实现前缀表达式计算:
用栈实现后缀表达式的运算
①、从右往左扫描下一个元素,直到处理完所有元素
②、若扫描到操作数则压入栈,并回到①;否则执行③
③、若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①
注意:
先出栈的是左操作数
3. 栈→递归
函数调用特点:
最后被调用的函数最先被执行结束。
求阶乘
int factorial(int n){
if(n==0||n==1){
return 1;
}
return n*factorial(n-1);
}
递归缺点:
效率低,太多层递归可能会导致栈溢出;可能包含很多重复计算
4. 队列
- 图的广度优先遍历
- 操纵系统进程调度算法:FCFS
- 页面替换算法的:FIFO
补充:
- 栈:迷宫求解;进栈转换;
- 队列:缓冲区;
- 递归一般效率都比较低,因为包含许多重复计算。
- 执行函数时,局部变量一般采用栈来存储。
- 消除递归通常需要栈来存储,也可以不用。