第三章栈和队列
- 栈
- 栈的基本概念
- 栈的顺序存储
- 栈的链式存储
- 队列
- 队列的基本概念
- 队列的顺序存储
- 循环队列
- 队列的链式存储
- 双端队列(选择题)
- 栈和队列的应用
- 常用应用
- 矩阵的压缩存储
栈
栈的基本概念
1.概念性质
栈:只允许在一端进行插入或删除操作的线性表
栈的特性:后进先出(LIFO)
栈的性质:n个不同元素进栈,出栈元素不同排列的个数为1/(n+1)Cn,2n(卡特兰数
2.基本操作
InitStack(&S)
StackEmpty(S)
Push(&S,x)
Pop(&S,&x)
GetTop(S,&x)
DestoryStack(&S)
栈的顺序存储
1.概念定义
采用顺序存储的栈成为顺序栈
栈顶指针:S.top
,初始时设置S.top = -1
栈顶元素:S.data[S.top]
进栈操作:栈不满时,栈顶指针先+1,再送值到栈顶元素
出栈操作:栈非空时,先取栈顶元素值,再将栈顶指针-1
栈空条件:S.top == -1
栈满条件:S.top == MaxSize-1
栈长:S.top+1
结构体
#define MaxSize 50
typedef struct{
ElemType data[MaxSize];
int top;
}SqStack;
2.顺序栈的基本运算
初始化
void InitStack(SqStack &S){
S.top == -1;
}
判栈空
bool StackEmpty(SqStack S){
return S.top == -1;
}
进栈
bool Push(SqStack &S,ElemType x){
if(S.top == MaxSize - 1)
return false;
S.data[++S.top] = x;
return true;
}
出栈
bool Pop(SqStack &S,ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top--];
return true;
}
读栈顶元素
bool GetTop(SqStack S,ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top];
return true;
}
3.共享栈(了解)
共享栈为了更有效的利用存储空间
结构体
#define MaxSize 10
typedef struct {
ElemType data[MaxSize];
int bottom;
int top;
}ShStack;
初始化栈
void InitStack(ShStack &S){
S.bottom = -1;
S.top = MaxSize;
}
栈满条件:bottom + 1 == top
栈的链式存储
1.概念定义
采用链式存储的栈成为链栈
链栈有点:便于多个栈共享存储空间和提高其效率,且不存在栈满溢出现象
结构体
typedef struct Linknode{
ElemType data;
struct Linknode *next;
}*LiStack;
2.基本操作
初始化
void InitStack(LiStack S){
S = NULL;
}
判空
bool StackEmpty(LiStack S){
return S == NULL;
}
队列
队列的基本概念
1.概念定义
队:只允许在表的一端进行插入,而在另一端进行删除
特性:先进先出(FIFO)
对头(front):允许删除的一端,又称队首
队尾(rear):允许插入的一端
2.基本操作
InitQueue(&Q):初始化
QueueEmpty(Q)
EnQueue(&Q,x)
DeQueue(&Q,&x)
GetHead(Q,&x)
队列的顺序存储
1.概念定义
初始状态(队空条件):Q.front == Q.rear == 0
进队操作:①队不满时②先送值到队尾元素③队尾指针+1
出队操作:①队不空时②先取队头元素值③队头指针+1
不能用Q.rear == MaxSize (×)
作为队列满条件
循环队列
1.概念定义
循环队列:把存储队列元素的表从逻辑上视为一个环
初始时Q.front =0,rear = 0
队首指针+1:Q.front = (Q.front + 1) % MaxSize
队尾指针+1:Q.rear = (Q.rear + 1) % MaxSize
队列长度:(Q.rear - Q.front + MaxSize) % MaxSize
2.判空判满条件:三种处理方式
①:牺牲一个单元
判满条件:(Q.rear + 1) % MaxSize
判空条件:Q.rear == Q.front
元素个数:(Q.rear - Q.front + MaxSize) % MaxSize
②:增设表示元素个数的变量
判满条件:Q.size == MaxSize
判空条件:Q.size == 0
元素个数:Q.size
③增设tag变量,0:删除操作;1:插入操作
判满条件:Q.rear == Q.front && tag == 1
判空条件:Q.rear == Q.front && tag == 0
元素个数:(Q.rear - Q.front + MaxSize) % MaxSize
3.基本操作(牺牲一个单元)
初始化
void InitQueue(SqQueue &Q){
Q.rear = Q.front = 0;
}
入队
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;
return true;
}
出队
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;
}
队列的链式存储
//结构定义
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front,*rear;
}LinkQueue;
带头结点
//初始化
void InitQueue(LinkQueue &Q){
Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
Q.rear->next = NULL;
}
//判断队列是否为空
bool Empty(LinkQueue Q){
return Q.rear == Q.front;
}
//新元素入队
void EnQueue(LinkQueue &Q,ElemType x){
LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
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 = p->data;
Q.front->next = p->next;
if(Q.rear == p)
Q.rear = Q.front;
free(p)
return true;
}
不带头结点
//初始化
void InitQueue(LinkQueue &Q){
Q.front = NULL;
Q.rear = NULL;
}
//判断队列是否为空
bool Empty(LinkQueue Q){
return Q.rear == NULL;
}
//新元素入队
void EnQueue(LinkQueue &Q,ElemType x){
LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
if(Q.rear == NULL){
Q.rear = Q.front = s;
}else{
Q.rear->next = s;
Q.rear = s;
}
}
//出队
bool DeQueue(LinkQueue &Q,ElemType &x){
if(Q.rear == NULL)
return false;
LinkNode *q = Q.front;
x = p->data;
Q.front = p->next;
if(q == Q.rear)
Q.rear = Q.front;
free(q);
return true;
}
双端队列(选择题)
输入受限的双端队列
输出受限的双端队列
在栈中合法的输出序列在双端队列中必定合法
栈和队列的应用
1.括号匹配
遇到左括号就入栈
遇到右括号就消耗一个左括号
扫描到右括号且栈为空—右括号单身
处理完所有括号后,栈非空—左括号单身
//算法实现
//分析:1.循环扫描括号序列,判断是左括号还是右括号:若是左括号,进栈
//2.若是右括号,先判断栈是否为空,为空则匹配失败;不为空则判断是否与栈顶元素匹配:匹配则继续循环,不匹配则匹配失败
//3.最终遍历完,判断栈是否为空,为空则匹配成功,不为空则匹配失败
bool bracketCheck(char str[],int length){
SqStack S;
InitStack(S);
for(int i = 0;i < length;i++){
if(str[i] == '(' || str == '[' || str == '{')
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.三种算数表达式
算术表达式有三部分组成:操作数、运算符、界限符:反映了计算的先后顺序
- 中缀表达式:a+b-c*d
- 后缀表达式(逆波兰表达式)★:ab+ cd* -
- 前缀表达式(波兰表达式):- +ab *cd
3.表达式之间转换 - 中缀表达式转后缀表达式
①确定运算符生效顺序
②按左操作数,右操作数,运算符的方式重新组合
左优先原则:只要左边的运算符能先算,就优先计算左边的
机算
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符
①遇到操作符:直接加入后缀表达式
②遇到界限符:若为‘ ( ’直接入栈,若为‘ ) ’则依次弹出栈内运算符并加入后缀表达式,直至弹出‘ ( ’为止(界限符不加入后缀表达式)
③遇到运算符:依次弹出栈内高于或等于当前运算符的所有运算符,并加入后缀表达式,直至遇到‘ ( ’或栈空则停止,再把当前运算符入栈
④处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式
- 中缀表达式转前缀表达式
①确定运算符生效顺序
②按运算符,左操作数,右操作数的方式重新组合
右优先原则:只要右边的运算符能先算,就优先计算右边的
4.表达式求值 - 后缀表达式求值(适用于栈)
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行运算,合为一个操作数
特点:最后出现的操作数先被运算
用栈实现
①从左往右扫描元素
②若扫描到操作数,则压入栈中并回到①
③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶
注:先出栈的是右操作数
- 前缀表达式求值
用栈实现
①从右往左扫描元素
②若扫描到操作数,则压入栈中并回到①
③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶
注:先出栈的是左操作数
- 中缀表达式求值
中缀转后缀 + 后缀表达式求值
用栈实现
初始化两个栈:操作数栈,运算符栈
若扫描到操作数,压入操作数栈
若扫描到运算符或界限符,则按照‘中缀转后缀’相同的逻辑压入运算符栈
(每当弹出一个运算符时,就需要在弹出两个操作数栈的栈顶元素,并执行相应运算,运算结果再压入操作数栈)
常用应用
1.栈在递归中的应用
可以把原始问题转换为属性相同,但规模较小的问题
太多层递归可能会导致栈溢出
- 递归
- 进制转换
- 表达式求值
- 迷宫求解
- 括号匹配
- 深度优先搜索
2.队列在操作系统中的应用
FCFS(先来先服务)争抢有限的系统资源
- CPU资源的分配
- 打印数据缓冲区
- 缓冲区
- 页面替换算法
- 广度优先搜索
矩阵的压缩存储
数组的存储结构
-
一维数组
数组元素A[i]的存放地址为
下标从0开始:LOC + i * sizeof(ElemType)
下标从1开始:LOC + (i - 1) * sizeof(ElemType)
-
二维数组
行优先存储
M行N列的二维数组b[M][N]中,b[i][j]的存储地址:LOC + (i * N + j) * sizeof(ElemType)
列优先存储
M行N列的二维数组b[M][N]中,b[i][j]的存储地址:LOC + (j * M + i) * sizeof(ElemType)
特殊矩阵
- 对称矩阵aij = aji
i<j 上三角区,i > j 下三角区
性质:aij = aji
只存储主对角线+下三角区
按行优先原则将各元素存入一维数组
1.数组大小:n(n + 1) / 2
映射关系:矩阵下标aij(i≥j) ➡一维数组下标(B[k])
2.aij是第几个元素:[1+2+…+(i - 1)] + j ➡第i(i-1)/2 + j个元素
3.数组下标k:k = i(i-1)/2 + j -1 (注:数组下标从0开始)
数组下标k:k = i(i-1)/2 + j (注:数组下标从1开始)
- 三角矩阵
压缩存储策略:按行优先原则将下三角区元素存入一维数组,并在最后一个位置存储常量C
和对称矩阵类似,只是多了一个存储常量的位置
- 三对角矩阵(带状矩阵)(当|i-j| > 1时,有aij = 0)
压缩存储策略:按行优先原则,只存储带状部分
映射关系:矩阵下标aij(|i - j| ≤ 1) ➡一维数组下标(B[k])
1.aij是第几个元素
分析过程:①前i-1行共3(i - 1) - 1个元素
②aij是第i行第j - i + 2 个元素
③aij是第2i + j - 2个元素
2.数组下标k:k = 2i + j - 3(数组下标从0开始)
3.已知数组下标k,如何得到ij
分析过程:①下标k即k+1个元素的i和j
②前i - 1行共3(i - 1) - 1;前i行共3i - 1个元素
③3(i - 1) - 1 < k + 1 ≤ 3i - 1
④i ≥ (k + 2) / 3
⑤i = 向上取整
- 稀疏矩阵
压缩存储策略:
顺序存储 — 三元组<行,列,值>(只存储非零元素)
链式存储 — 十字链表法