栈 和 队列
栈
基本概念
定义
- 栈 Stack 是 只允许一端进行插入或删除操作 的 线性表
- 后进先出 LIFO
- 栈顶
- 栈底
- 空栈
基本操作
- 创建 InitStack(&S)
- 销毁 DestoryStack(&L)
- 进栈 Push(&S, x) 增
- 出栈 Pop(S, &x) 删
- 查找 Get(S,&x)
- 常考 进栈后 合法的出栈顺序,记住 后进先出
- n 个不同元素进栈,出栈元素的不同排列的个数$\frac{1}{n+1}C^n_{2n} $ 该公式为 卡特兰数
基本实现
顺序栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
#define MaxSize 10 // 定义 栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; // 静态数组 存放 栈中元素
int top; // 栈顶指针
}SqStack;
// 初始化 栈
void InitStack(SqStack &S){
S.top = -1; // 初始化 栈顶指针
// 另一种初始化 是 S.top = 0;
// 对应第一种方式 S.data[S.top] 是无效的 要到data[0] 要先把 S.top + 1
// 第二种方式 S.data[S.top] = S.data[0]
// 所以 要 注意审题 题目中的 初始化不同 后续的出入站代码会有区别
}
// 判断 栈空
bool StackEmpty(SqStack S){
if(S.top == -1)
return true;
else
return false;
}
// 入栈 Push
bool Push(SqStack &S, ElemType x){
if(S.top==MaxSize-1)
return false; // 栈满
S.top = S.top + 1; // 指针先 + 1
S.data[S.top] = x; // 新元素入栈
// 指针+1 新元素入栈 两行 等价于 下面一行被注释的代码
// S.data[++S.top] = x;
return true;
}
// 出栈 Pop
bool Pop(SqStack S, ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top];
S.top = S.top - 1;
// 同样 上述两行代码等价于 下面一行被注释 的代码
// x = S.data[S.top--];
return true;
}
// 读取栈顶
bool GetPop(SqStack S, ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top]; // 用 x 返回栈顶元素
return true;
}
void testStack(){
SqStack S; // 声明一个顺序栈
InitStack(S);
// ...
}
|
共享栈
1
2
3
4
5
6
7
8
9
10
11
12
|
#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;
}
|
- 销毁
- 静态分配:栈顶 指针 返回 到初始化的位置
- 动态分配:先释放内存 再 返回指针
链式栈
类似于 头插法 单链表,链头作为栈顶,只不过 只能在栈顶进行操作
TODO 使用 带头结点 和不带 头结点的都需要会写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
// 带头结点的链式栈
// S.top 是 固定的头结点
typedef struct StackNode{
ElemType *data;
struct StackNode *next;
} StackNode;
typedef struct{
StackNode *top;
}LinkStack;
bool InitLStack(LinkStack &S){
S.top = (StackNode *)malloc(sizeof(StackNode));
if(!S.top) return false;
S.top->next = NULL;
return true;
}
bool Push(LinkStack &S, ElemType e){
StackNode *p = (StackNode *)malloc(sizeof(StackNode));
if(!p) return false;
p->data = e;
p->next = S.top->next; // 新结点 指向原来的栈顶
S.top->next = p; // 头结点 指向 新的栈顶
return true;
}
bool Pop(LinkStack &S, ElemType &e){
if(S.top->next==NULL) return false;
StackNode *p = S.top->next;
e = p->data;
S.top->next = p->next;
free(p);
return true;
}
bool GetTop(LinkStack S, ElemType &x){
if(S.top->next==NULL) return false;
x = S.top->next->data;
return true;
}
bool DestroyStack(LinkStack &S) {
// 1. 暂存头结点的后继(栈顶元素结点),作为遍历起点
StackNode *p = S.top->next;
// 2. 遍历释放所有数据结点(和不带头结点的遍历逻辑一致)
while (p) {
S.top->next = p->next; // 头结点指向当前结点的后继
free(p); // 释放当前数据结点
p = S.top->next; // 移动到下一个数据结点
}
// 3. 释放头结点(核心!不带头结点无此步骤)
free(S.top);
// 4. 栈顶指针置空(避免野指针,考研采分点)
S.top = NULL;
return true;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
// 不带头结点的链式栈
// S.top 是 栈顶结点
typedef struct StackNode{
ElemType *data;
struct StackNode *next;
} StackNode;
typedef struct{
StackNode *top;
}LinkStack;
bool InitLStack(LinkStack &S){
S.top = NULL;
return true;
}
bool Push(LinkStack &S, ElemType e){
StackNode *p = (StackNode *)malloc(sizeof(StackNode));
if(!p) return false;
p->data = e;
p->next = S.top;
S.top = p;
return true;
}
bool Pop(LinkStack &S, ElemType &e){
if(S.top==NULL) return false;
StackNode *p = S.top;
e = p->data;
S.top = p->next;
free(p);
return true;
}
bool GetTop(LinkStack S, ElemType &x){
if(S.top==NULL) return false; // 空栈判空
x = S.top->data;
return true;
}
bool DestroyStack(LinkStack &S) {
StackNode *p = S.top;
while(p) {
S.top = p->next;
free(p);
p = S.top;
}
return true;
}
|
栈 的应用—括号匹配问题
- 最后 出现的 左括号 优先 被匹配 LIFO
- 解决思路
- 遇到 左括号 就 入栈
- 遇到 右括号 就 “消耗” 一个 左括号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
#define MaxSize 10
typedef struct{
char data[MaxSize];
int top;
}SqStack;
// 初始化栈
InitStack(SqStack &S)
// 判断栈是否为空
bool IsEmpty(SqStack S)
// 新元素入栈
bool Push(SqStack &S, char x)
// 栈顶元素出栈 用 x返回
bool Pop(SqStack &S, char &x)
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(IsEmpty(S)) // 扫描到右括号 且当前栈空
return false // 匹配失败
char topElem;
Pop(topElem);
if(str[i]==')' && topElem!='(')
return false;
if(str[i]==']' && topElem!='[')
return false;
if(str[i]=='}' && topElem!='{')
return false;
}
}
return IsEmpty(S); // 检索完全部括号后,栈空 说明匹配成功
}
|
TODO 不用 基础操作,实现全部代码
栈的应用—表达式求值
三种算术表达式
- 中缀表达式
- 由 操作数、运算符、界限符(必不可少的、反映计算的先后顺序) 组成
- 规则:运算符 在两个操作数 中间,如 a+b
如何不用 界限符 也能准确表达运算顺序:
-
后缀表达式 逆波兰表达式
- 规则:运算符在两个操作数后面,如 ab+
- 中缀 转 后缀
- 确定 中缀表达式中 各个运算符的运算顺序
- 选择下一个运算符,按照 左操作数 右操作数 运算符 的方式 组合成一个新的操作数
- 如果还有运算符没被 处理 ,就继续第2步
- 后缀表达式 中 从左到右出现的运算符就是 中缀表达式中 按照先后顺序执行的运算符顺序
- 如果 运算顺序不唯一,后缀表达式 也不唯一
- 要注意 左优先 原则
- 用 栈 实现的计算思路
- 从左往右 扫描下一个元素,直到处理完所有元素
- 若 扫描到 操作数 则压入栈,并回到 1,否则执行 3
- 若扫描到运算符,则弹出 两个 栈顶 元素,执行相应运算,运算结果压会栈顶,回到 1
- 注意 先出栈 的 右操作数
- 若表达式 合法,则最后栈中只会留下一个元素 即最终结果
- 中缀表达式 转 后缀表达式(代码思路)
- 初始化一个栈,用于保存 暂时还不能确定运算顺序 的 运算符,从左到右处理各个元素,直到末尾。可能遇到 三种情况:
- 遇到 操作数 直接加入后缀表达式
- 遇到 界限符 。遇到 “(” 直接入栈,遇到")“则依次弹出 栈内运算符 并加入后缀表达式,直到弹出 “(“为止。注:”(“不加入 后缀表达式
- 遇到 运算符。依次弹出 栈中 优先级 高于 或等于 当前运算符 的 所有运算符,并加入后缀表达式,若碰到 “(” 或者 栈空则停止,之后再把当前运算符入栈
- 中缀表达式的计算(用栈 实现)
- 初始化 两个栈,操作数栈 和 运算符栈
- 若扫描到操作数,压入操作数栈
- 若扫描到 运算符或界限符,按照中缀转后缀 相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
- 本质 是用 中缀 转 后缀 + 后缀 表达式求值 两个算法的结合
-
前缀表达式 波兰表达式
-
规则:运算符在两个操作数后面,如 +ab
-
中缀 转 前缀
- 确定 中缀表达式 中 各个运算符 的 运算顺序
- 选择下一个运算符 , 按照 运算符 左操作数 右操作数 的方式组合成一个新操作数
- 如果还有 运算符 未被处理,就继续 2
栈的应用—递归
- 函数调用 的 特点
- 函数 调用栈 (函数调用时 需要 一个栈 来存储):
- 递归:把原始问题 转化为 属性相同,但规模较小的问题
- 递归调用 时,函数调用栈 可称为"递归工作栈” 每进入一层递归,就将递归调用所需信息压入栈顶,每退出一层递归,就从栈顶弹出相应的信息
- 缺点:效率低,太多层递归可能会导致栈溢出:可能包含很多重复计算
- 可以自定义栈 将 递归算法改造成 非递归算法
队列
定义
- 只允许在一端(队尾)进行插入(入队),另一端(队头)进行删除(出队) 的 线性表
- 特点: 先进先出 FIFO
基本操作
顺序存储实现队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
#define MaxSize 10
typedef strcut{
Elemtype data[MaxSize]; // 用静态数组存放队列元素
int front,rear; // 队头指针和队尾指针
// 为解决 牺牲一个存储空间 来判断 队满 还是队空
// 第一种 方法
int size; // 记录队列当前长度
// 插入成功 size++
// 删除成功 size--
// 用size判断 队满(size==MaxSize) 和 队空(size==0)
// 第二种 方法
int tag; // 用于记录 最近进行的操作是 删除还是插入
// 每次删除操作成功,tag =0;
// 每次插入操作成功,tag =1;
// 队满条件 front == rear && tag==1
// 队空条件 front == rear && tag==0
} SqQueue;
void InitQueue(SqQueue &Q){
// 初始化时 队头、队尾指针 指向 0
Q.rear=Q.front=0;
size = 0;
}
// 以下代码是基于 队尾指针 指向 队尾元素下一个位置
// 根据 题目要求不同 如 队尾指针就是指向 队尾元素 相关操作代码要改变
// 初始化时 rear = MaxSize-1
// 入队操作
bool EnQueue(SqQueue &Q, ElemType x){
if((Q.rear+1)%MaxSize==Q.front) // 队列已满
// 队列已满的条件:队尾指针的在下一个位置 是队头
return false;
Q.data[Q.rear] = x; // 将x插入队尾
// 队尾指针 指向 下一个待插入元素的位置
Q.rear = (Q.rear+1)%MaxSize; // 队尾指针后移 队尾指针+1 再取模
// 模运算 将 存储空间 在逻辑上变成了环状 牺牲了一个存储空间
// 此时的队列也叫循环队列
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;
}
// 查找操作 获取队头元素
bool GetHead(SqQueue Q, ElemType &x){
if(Q.rear==Q.front) // 判断 队列为空
return false
x = Q.data[Q.front]
return true;
}
// 队列的元素 个数 计算:(rear + MaxSize - front) % MaxSize
void testQueue(){
SqQueue Q; // 声明一个队列
InitQueue(Q); // 初始化队列
//...
}
|
TODO: 以下6中情况 分别分析
- rear指向 队尾元素下一个位置
- 牺牲一个存储单元
- 增加size变量 记录 队列长度
- 增加tag变量 用于记录近一次操作是 删/插
- rear 指向队尾元素
- 牺牲一个存储单元
- 增加size变量 记录 队列长度
- 增加tag变量 用于记录近一次操作是 删/插
链式存储实现队列
- 一般情况下 链式队列 不易出现队满的情况,除非内存不足
- 要注意题目 中 是带头结点 还是不带头结点
- 数据操作要注意边界条件,以及 处理第一个/最后一个元素入/出 队的情况
带头结点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
typedef struct LinkNode{ // 链式队列结点
ElemType data;
struct LinkNode *next;
} LinkNode;
typedef struct{ // 链式队列
LinkNode *front, *rear;
}LinkQueue;
void InitQueue(LinkQueue &Q){
// 初始化时 front rear 都指向头结点
Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode));
Q.front->next = NULL;
}
bool IsEmpty(LinkQueue Q){
if(Q.fornt==Q.rear)
return false;
else
return true;
}
// 入队操作
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; // 修改表尾指针
}
// 出队操作
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 ture;
}
void test(){
LinkQueue Q;
InitQueue(Q);
}
|
不带头结点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
typedef struct LinkNode{ // 链式队列结点
ElemType data;
struct LinkNode *next;
} LinkNode;
typedef struct{ // 链式队列
LinkNode *front, *rear;
}LinkQueue;
void InitQueue(LinkQueue &Q){
// 初始化时 front rear 都指向头结点
Q.front = NULL;
Q.rear = NULL;
}
bool IsEmpty(LinkQueue Q){
if(Q.front==NULL)
return true;
else retrun false;
}
// 入队操作
void EnQueue(LinkQueue &Q, Elemtype x){
LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
if(Q.frong==NULL){ // 空队列中插入第一个元素
Q.front = s; // 修改头尾指针
Q.rear = s;
}esle{
Q.rear->next = s;
Q.rear = s;
}
Q.rear->next = s; // 新结点 插入到 rear 之后
Q.rear = s; // 修改表尾指针
}
// 出队操作
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front==NULL) // 判断空队列
return false;
LinkNode *p = Q.front; // p指向 此次出队的结点
x = p->data; // 用x返回 队头元素
Q.front = p->next; // 修改 front 指针
if(Q.rear==p) // 最后一个结点 出队
Q.rear=NULL;
Q.front=NULL;
free(p);
return ture;
}
|
- 如果需要 知道 队列长度
- 可循环遍历队列
- 或 经常访问队列长度 时 增加一个 length 变量记录,当前队列长度
双端队列
考点
判断输出序列的合法性
类似于 栈 中判断输出序列合法性的判断
队列应用
特殊矩阵的压缩存储
数组的存储结构
- 数据各个元素 大小相同,且物理上连续存储
- 除特别说明,否则数组下标默认从0开始
一维数组
- 数据元素
a[i]的存放地址 = LOC + i* sizeof(ElemType)
二维数组
M行N列 的 二维数组 b[M][N]中
- 物理结构
- 行优先存储
b[i][j]的存储地址 = LOC+(i*N+j)*sizeof(ElemType)
- 列优先存储
b[i][j]的存储地址 = LOC+(i+j*N)*sizeof(ElemType)
普通矩阵的存储
-
注意
- 描述 矩阵元素时,行、列通常从1开始
- 而描述数组时通常下标从0开始
- 具体看题目所给条件,注意审题
-
可用 二维数组存储
特殊矩阵
对称矩阵
-
定义
- 若n阶 方阵 中 任意一个元素 $a{_i},{_j}$
- 都有 $a{_i},{_j} = a{_j},{_i}$
-
压缩存储策略
-
只存储 主对角线 和 下/上三角区
-
按 行优先 原则 将各元素存入 一维数组中
- 下三角区 $i>=j$
- 数组大小:$\frac{n*(n+1)}{2}-1$ ,数组下标 从0开始
- $a{_i},{_j}$ 是第几个元素?
- $\frac{i*(i+1)}{2}+j$ 个元素
- 数组下标 $k =\frac{i*(i+1)}{2}+j-1$
- 数组下标 k
- 下三角区 $k =\frac{i*(i+1)}{2}+j-1$
- 上三角区 $k =\frac{j*(j+1)}{2}+i-1$
-
按 列 有限原则
数组下标 k
- 下三角区 $k =\frac{j*(2n-j+1)}{2}+(i-j)$
- 上三角区 $k =\frac{i*(2n-i+1)}{2}+(j-i)$
三角矩阵
上三角矩阵
下三角矩阵
压缩存储策略
- 按行优先原则,将主对角线和 上/下 三角区 的元素存入一维数组
三对角矩阵
- 也叫 带状矩阵
- 当 $|i-j|>1$时,有 $a{_i},{_j}=0$ (i>=1, j<=n)
- 主对角线元素是非零元素,且主对角线的 上下左右的元素是非零元素,其余元素都是0
- 压缩存储策略
- 一共要存放 3n-2个元素,最后一个元素的数据下标是 3n-3
- 按行优先原则,只存储非零元素
稀疏矩阵
- 非零元素 的 个数 远远少于 矩阵元素的个数
- 压缩存储策略