栈和队列
考纲内容
- 栈和队列的基本概念
- 栈和队列的顺序存储结构
- 栈和队列的链式存储结构
- 栈和队列的应用
- 特殊矩阵的压缩存储
要求内容
- 栈和队列各种存储结构的特点
- 数组和特殊矩阵的压缩存储
- 思维导图索引
1.栈
1.1 栈的基本概念
1.1.1栈的定义
- 栈 (Stack) 是只允许在一端进行插入或者删除操作的线性表
- 栈顶 (Top) 。线性表允许进行插入删除的那一端
- 栈底 (Bottom) 固定的,不允许插入和删除的另一端
- 空栈 。不含任何元素的空表
如上,假设栈
S
S
S = (
a
1
,
a
2
,
a
3
,
a
4
,
a
5
a_1,a_2,a_3,a_4,a_5
a1,a2,a3,a4,a5),
a
1
a_1
a1为栈底元素,
a
5
a_5
a5为栈顶元素。由于栈只能在栈顶 进行插入和删除操作,进栈顺序为
a
1
,
a
2
,
a
3
,
a
4
,
a
5
a_1,a_2,a_3,a_4,a_5
a1,a2,a3,a4,a5,出栈顺序为
a
5
,
a
4
,
a
3
,
a
2
,
a
1
a_5,a_4,a_3,a_2,a_1
a5,a4,a3,a2,a1,操作特性为后进先出 (Last In First Out,LIFO)
- 栈的数学性质: n n n 个不同的元素进栈,出栈元素不同的排列个数为 1 n + 1 C 2 n n \frac{1}{n+1} C^{n}_{2n} n+11C2nn,该公式称为卡特兰数( C a t a l a n Catalan Catalan)
1.1.2 栈的基本操作
InitStack (&S):初始化一个栈
StackEmpty (S):判断栈是否为空,为空放回true,否则返回false
Push (&S,x):进栈,若栈没满,则加入元素x成为新栈顶
Pop (&S,&x):出栈,若栈非空,则弹出栈顶元素并用x返回
GetTop (S,&x): 读取栈顶元素,若栈非空则用x返回栈顶元素
DestroyStack (&S):销毁栈并释放存储空间
算法题中,若题干没有做限制,可直接使用基本的操作函数
1.2 栈的顺序存储结构
1.2.1 顺序栈的实现
- 采用顺序存储的栈称为顺序栈,利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时设一个指针(top)指向当前栈顶的元素
栈的存储类型描述
#define MaxSize 50 //定义栈中元素最大个数
typedef struct{
Elemtype data[MaxSize]; //存放栈中元素
int top; //栈顶元素
}SqStack;
- 栈顶指针:S.top,初始设置S.top= -1(有些辅导书从0开始);栈顶元素S.data[S.top]
- 进栈操作,栈不满时,栈顶指针先加 1 ,再送值到栈顶元素
- 出栈操作:先取栈顶元素,再将栈顶指针减1
- 栈空条件:S.top=-1; 栈满条件:S.top==MaxSize-1; 栈长:S.top+1
1.2.2 顺序栈的基本运算
(1)初始化
void InistStack(Sqstack &S){
S.top=-1; //初始化栈顶指针
}
(2)判断空
bool StackEmpty(Sqstack &S){
if(S.top==-1) //栈空
return true;
else //栈不空
return false;
(3)进栈
S.top =S.top+1 和 S.data[S.top] = x等价于S.data[++S.top]=x;
bool Push(SqStack &S,ElemType x){
if(S.top==MaxSize-1) //栈满,报错
return false;
S.data[++S.top]=x; //指针先加1,再入栈
return true;
}
(4)出栈
S.data[S.top] = x和 S.top =S.top-1 等价于x=S.data[S.top-- ];
bool Pop(SqStack &S.ElemType x){
if(S.top==-1) //栈空,报错
return false;
x=S.data[S.top--]; //先出栈,指针再减1
return true;
}
(5)读栈顶元素
仅读取栈顶元素,并没有执行出栈操作,原栈顶元素依然保留在栈中
bool GetTop(Sqstack S,ElemType &x){
if(S.top==-1) //栈空,报错
return false;
x=S.data[S.top]; //x记录栈顶元素
return true;
注意
- 这里的top指向的是栈顶元素,所以进栈操作为S.data[++S.top]=x,出栈操作为x=S.data[S.top --];
- 若栈顶指针初始为S.top=0,则top指向栈顶元素的下一个位置,这样入栈操作变为 S.data[S.top++]=x,先入栈,指针再加1;出栈操作变为 x=S.data[ --S.top],指针先减1,再出栈
1.2.3 共享栈
- 利用栈底位置相对不变的特性,可以让两个顺序栈共享一个一维数组空间
- 把两个栈的栈底分别设置在共享空间的两端,然后两个栈顶向共享空间的中间延伸
- 两个栈的栈顶指针都指向栈顶元素,top0=-1时0号栈为空,top1=MaxSize时1号栈为空
- 仅当两个栈顶指针相邻(top1-top0=1)时,为栈满
- 0号栈进栈时栈顶指针top0先加1再赋值,1号栈进栈时top1先减1再赋值,出栈时则刚好相反
- 共享栈是为了更有效的利用存储空间,两个栈的空间相互调节,整个存储空间被占满才会上溢,存取数据的时间复杂度为 O ( 1 ) O(1) O(1)
其定义为
#define MaxSize 50 //两个共享栈所能达到的最大长度
#define Elemtype int //假设元素类型为整数型
typedef struct{
Elemtype stack [MaxSize];
int top[2];//top为两个栈顶指针
}stk;
stk s; //定义的结构体变量
(1)入栈操作
int Push(int i,Elemtype x){
//i为栈号,i=0为左边的栈top0,i=1为右边的栈top1
//入栈成功返回1,否则返回0
if(i<0||i>1){
printf("栈号输入错误");
exit(0);
}
if(s.top[1]-s.top[0]==1){
printf("栈已满");
return 0;
}
switch(i){
case 0: s.stack[++s.top[0]]=x; return 1; break;
case 1: s.stack[--s.top[1]]=x; return 1;
}
}
(2)退栈操作
//退栈成功返回退栈元素,否则返回-1
int Pop(int i,Elemtype x){
if(i<0||i>1){
printf("栈号输入错误");
exit(0);
}
switch(i){
case 0:
if(s.top[0]==1){
printf("栈空");
return -1;
}
else
return s.stack[s.top[0]--];
case 1:
if(s.top[1]==MaxSize){
printf("栈空");
return -1;
}
else
return s.stack[s.top[1]++];
}//switch
}
2.队列
2.1 队列的基本概念
2.1.1 队列的定义
队列也是一种操作受限的线性表,只允许在一端进行插入,而在另一端进行删除。其操作特性是先进先出 (First In First Out,FIFO)
- 队头(Front) 允许删除的一端
- 队尾(Rear) 允许插入的一端
2.1.2 队列常见的基本操作
InitQueue (&Q) :初始化队列,构造一个空队列Q
QueueEmpty (Q) :判断列空,若队列Q为空返回true,否则返回false
EnQueue (&Q,x) : 入队,若队列Q未满,将x加入,使之成为新的队尾
DeQueue (&Q,&x) : 出队,若队列非空,删除队头元素并用x返回
GetHead (Q,&x) : 读取队头元素,若队列Q非空,则将队头元素赋值给x
2.2 队列的顺序存储结构
2.2.1 队列的顺序存储
队列的顺序存储类型定义
#define Maxsize 50 //定义队列中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //存放队列元素
int front,rear; //队头指针和队尾指针
}SqQueue;
2.2.2 循环队列
- 当队首指针Q.front=MaxSize-1后再前进一个位置就取余到0了
- 初始时:Q.front=Q.rear=0
- 队首指针进1:Q.front=(Q.front+1)%MaxSize
- 队尾指针进1:Q.rear=(Q.rear+1)%MaxSize
- 队列长度:(Q.rear+MaxSize-Q.front)%MaxSize
- 出队入队时:指针都按顺时针方向进1
如下图
区分队空还是队满有三种处理方式
(1)牺牲一个单元来区分队空和队满,入队(rear)时少用一个队列单元,设定以“队头指针(front)在队尾指针(rear)的下一个位置作为队满标志”,如上图的d2
- 队满条件:(Q.rear+1)%MaxSize==Q.front
- 队空条件仍为:Q.front==Q.rear
- 队列中元素的个数:(Q.rear+MaxSize-Q.front)%MaxSize
(2)增加一个设定表示元素个数的数据成员size,队空条件为Q.size==0,队满条件为Q.size==MaxSize
(3)增加设定一个tag数据成员,tag等于0时,因删除导致Q.front==Q.rear,则为队空;tag等于1时,因插入导致Q.front==Q.rear,则为队满
还有其他考法例如
(1)入队的一端,指针指向队尾元素的后一个位置(下一个应该插入的位置),那么先入队,再移动指针
(2)若入队的一端,指向当前元素,那么先移动指针再入队
2.2.3 循环队列的操作
(1)初始化
void InitQueue(SqQueue Q){
Q.rear==Q.front=0; //初始化队首,队尾指针
}
(2)判队空
bool IsEmpty(SqQueue Q){
if(Q.rear==Q.front) return true; //队空条件
else
return false;
}
(3)入队
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)出队
bool DeQueue(SqQueue &Q,ElemType &x){
if(Q.rear==Q.front) return false ; //队空则报错
x=Q.data[Q.front];
Q.front=(Q.front+1)%MaxSize; //队头指针加1取模
return true;
}
2.3 队列的链式存储结构
2.3.1队列的链式存储
一个同时带有队头指针和队尾指针的单链表。头指针指向队头的2位置,尾指针指向队尾的位置,也就是单链表的最后一个结点,链式队列不会出现分配不合理和溢出的问题
图为不带头结点的链式队列
当然带头结点的链表更方便
队列的链式队列存储类型描述为
typedef struct{ //链式队列结点
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{ //链式队列
LinkNode *front,*rear; //队列的队头和队尾指针
}LinkQueue;
2.3.2 链式队列的基本操作
(1)初始化
void InitQueue(LinkQueue &Q){
Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode)); //建立头结点
Q.front->next=NULL; //初始化为空
(2)判队空
bool IsEmpty(LinkQueue Q){
if(Q.rear==Q.front) return true; //队空条件
else
return false;
}
(3)入队
- ①入队时,建立一个新结点,将新结点插入链表的尾部,并改成让队尾指针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;
}
(4)出队
- ①出队时,首先判断队是否为空,不空则从链表中取出队头元素,并让队头指针Q.front指向下一个结点
- ②若该结点为最后一个结点,则置Q.front 和Q.rear都为NULL。
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;
}
2.4 双端队列
双端队列是指允许两端都可以进行入队和出队操作的队列,将队列的两端分别称为前端和后端,两端都可以入队和出队
- 输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入的双端队列
- 输入受限的双端队列:允许在一端进行插入和删除,但在另一端只允许删除的双端队列
3.栈和队列的应用
3.1栈在括号匹配中的应用
假设表达式中允许包含三种种括号:圆括号和方括号和花括号,其嵌套的顺序任意即 ([{}]) 或者 [({}{})] 均为正确的格式,[({]})、({]) 等均为不合法
算法思想如下:
- ①初始设置一个空栈,顺序读入括号
- ②若是右括号,一种则是栈中有元素且先弹出在栈顶最先需要匹配得以消去,另一种则是栈中有元素但括号不匹配或者空栈导致括号不匹配而结束程序
- ③若是左括号,则作为一个新的需要压入栈中,自然使得原有在栈中还没得以匹配消去的的左括号优先级降一级,让后进来的左括号优先匹配,符合栈的后进先出特性
- ④算法结束时,栈为空则匹配成功,否则括号序列不匹配
3.2栈在表达式求值中的应用
表达式求值是栈应用的一个典型范例,中缀表达式不仅依赖运算符的优先级,还要处理括号。后缀表达式的运算符在操作数的后面,在后缀表达式中已考虑了运算符的优先级,没有括号,只有操作数和运算符
- 后缀表达式=逆波兰表达式
- 前缀表达式=波兰表达式
算数表达式=操作数+运算符(±*/)+界限符(括号)
3.2.1 中缀表达式转后缀表达式(手算)
3.2.2 中缀表达式转后缀表达式(机算)-左优先
两点需要注意
- 扫描到暂时还不确定运算顺序的运算符要放在栈中
- 遇到运算符,依次弹出栈中优先级高于或等于当前运算符的所有运算符,加入后缀表达式
- 优先级: */ 优先级高于 + -
3.2.3 后缀表达式的计算(手算)
3.2.4 后缀表达式的计算(机算)
需要注意的地方是
- 从左往右扫描,压入栈中的是操作数
- 扫描的运算符,则弹出两个栈顶元素
- 先出栈的当作右操作数,在运算符的右边
3.2.5 中缀表达式转前缀表达式(手算)-右优先
3.2.6 前缀表达式的计算
需要注意的地方是,
- 从右往左扫描,压入栈中的是操作数
- 扫描的运算符,则弹出两个栈顶元素
- 先出栈的当作左操作数,在运算符的左边
3.2.7 中缀表达式的计算
- 中缀表达式 = 中缀转后缀 + 后缀表达式计算
- 两个栈:操作数栈和运算符栈
3.2.7 总结
3.3栈在递归中的应用
3.4栈在层次遍历中的应用
- 二叉树中再学
3.5队列在计算机系统中的应用
4.特殊矩阵的压缩矩阵
4.1 数组的存储结构
4.1.1 一维数组的存储结构
- 以一维数组A[0…n-1]为例,存储结构关系式为
L O C ( a i ) = L O C ( a 0 ) + i × L ( 0 ≤ i < n ) LOC(a{_i})=LOC(a{_0}) +i × L(0≤i<n) LOC(ai)=LOC(a0)+i×L(0≤i<n)
- a 0 a{_0} a0为起始地址, i i i为元素在数组中的位置, L L L 则是每个数组元素所占的存储单元
- 除非题目说明,否则数组下标默认从0开始
4.1.2 多维数组的存储结构
以二维数组为例,两种映射方法:按行优先和按列优先
(1)行优先存储
先行后列,先存储行号较小的元素,行号相等的先存储列号较小的元素
- 一种是王道书按二维数组行下标和列下标来计算地址,二维数组的行下标和列下标的范围分别是
[
0
,
h
1
]
[0,h{_1}]
[0,h1]与
[
0
,
h
2
]
[0,h{_2}]
[0,h2],则存储结构关系为
从数组下标0开始
L O C ( a i , j ) = L O C ( a 0 , 0 ) + [ i × ( h 2 + 1 ) + j ] × L LOC(a{_i,_j})=LOC(a{_0,_0}) +[i ×(h{_2}+1)+j]×L LOC(ai,j)=LOC(a0,0)+[i×(h2+1)+j]×L
从行与列的位序开始
L O C ( a i , j ) = L O C ( a 0 , 0 ) + ( i × N + j ) × L LOC(a{_i,_j})=LOC(a{_0,_0}) +(i ×N+j)×L LOC(ai,j)=LOC(a0,0)+(i×N+j)×L
其实书上也没有错,书上按了数组的方式
例如,对于数组
A
23
A{_{23}}
A23按行优先在内存的存储形式如下
两行三列,分别对应数组中[0,1]一共两行和数组中[0,2]一共三列,而根据公式算出的位置是按位序从1开始,数组下标从0开始,所以
h
2
h_2
h2要加1
- 另一种按行和列的下标开始,则计算的时候不用加1,所以要注意是从位序开始还是从数组下标开始
(2)列优先存储
列优先存储结构关系式
从数组下标0开始
L
O
C
(
a
i
,
j
)
=
L
O
C
(
a
0
,
0
)
+
[
j
×
(
h
1
+
1
)
+
i
]
×
L
LOC(a{_{i,j}})=LOC(a{_{0,0}}) +[j×(h{_1}+1)+i]×L
LOC(ai,j)=LOC(a0,0)+[j×(h1+1)+i]×L
从行与列的位序开始
L
O
C
(
a
i
,
j
)
=
L
O
C
(
a
0
,
0
)
+
(
j
×
N
+
i
)
×
L
LOC(a{_{i,j}})=LOC(a{_{0,0}}) +(j×N+i)×L
LOC(ai,j)=LOC(a0,0)+(j×N+i)×L
对于
A
23
A{_{23}}
A23,同理
4.2 矩阵的压缩存储
- 注意
- 描述矩阵元素时,行、列号通常从1开始
- 描述数组时,下标通常从0开始
- 目的是为了节省存储空间
4.2.1 对称矩阵的压缩存储
- 对于一个 n n n 阶方阵 A [ 1... n ] [ 1... n ] A[1...n][1...n] A[1...n][1...n] 中任意一个元素 a i j a{_{ij}} aij 都有 a i j = a j i ( 1 ≤ i , j ≤ n ) a{_{ij}}=a{_{ji}}(1≤i,j≤n) aij=aji(1≤i,j≤n),则称为对称矩阵,其中元素划分为3个部分,即上三角区,主对角线和下三角区
- 对于 n n n 阶对称矩阵,上三角区的元素·和下三角区的元素对应,若是采用二维数组存储,几乎浪费一半的空间
将对称矩阵
A
[
1...
n
]
[
1...
n
]
A[1...n][1...n]
A[1...n][1...n]存放一维数组
B
[
n
×
(
n
+
1
)
2
]
B[\frac{n×(n+1)}{2}]
B[2n×(n+1)]中,有两种策略
只存储主对角线+下三角区或者主对角线+上三角区
- 矩阵下标转换为一维数组下标从0开始,要减一
按行优先原则,在数组中
- 第 1 1 1 行: 1 1 1个元素( a 1 , 1 a{_{1,1}} a1,1)
- 第
2
2
2 行:
1
1
1个元素(
a
2
,
1
,
a
2
,
2
a{_{2,1}},a{_{2,2}}
a2,1,a2,2)
. . . . . ..... ..... - 第 i − 1 i-1 i−1行: i − 1 i-1 i−1个元素:( a i − 1 , 1 , a i − 1 , 2 , . . . . a i − 1 , i − 1 a{_{i-1,1}},a{_{i-1,2}},....a{_{i-1,i-1}} ai−1,1,ai−1,2,....ai−1,i−1)
- 第
i
i
i行: 因为最后一行变化的下标
j
j
j列,且数组下标从0开始
所以是 j − 1 j-1 j−1个元素:( a i , 1 , a i , 2 , . . . . a i , j − 1 a{_{i,1}},a{_{i,2}},....a{_{i,j-1}} ai,1,ai,2,....ai,j−1)
因此元素在数组中的下标
K
=
1
+
2
+
.
.
.
+
(
i
−
1
)
+
j
−
1
=
i
×
(
i
−
1
)
2
+
j
−
1
K=1+2+...+(i-1)+j-1= \frac{i×(i-1)}{2}+j-1
K=1+2+...+(i−1)+j−1=2i×(i−1)+j−1
以上推导从数组下标
0
0
0开始,当数组下标从
1
1
1开始,可以采用同样的推导方法
4.2.2 三角矩阵的压缩存储
(1)下三角矩阵
上三角区的所有元素均为同一常量,与对称矩阵类似,不同之处是存完下三角区和主对角线元素后,接着存储上三角区的常量一次。所以可以将下三角矩阵 A [ 1... n ] [ 1... n ] A[1...n][1...n] A[1...n][1...n]按矩阵元素压缩在 B [ n × ( n + 1 ) 2 + 1 ] B[\frac{n×(n+1)}{2}+1] B[2n×(n+1)+1]中
数组下标从0开始,则对应数组下标
(1)上三角矩阵
下三角区的所有元素均为同一常量,只需存储主对角线、上三角区的元素和下三角区的常量一次。所以可以将下三角矩阵 A [ 1... n ] [ 1... n ] A[1...n][1...n] A[1...n][1...n]按矩阵元素压缩在 B [ n × ( n + 1 ) 2 + 1 ] B[\frac{n×(n+1)}{2}+1] B[2n×(n+1)+1]中
在数组中,位于元素 a i j ( i < j ) a{_{ij}}(i<j) aij(i<j)前面(不包括 a i j a{_{ij}} aij)的元素个数(不是数组下标)为
- 第 1 1 1 行: n n n个元素( n − 1 + 1 n-1+1 n−1+1)
- 第
2
2
2 行:
n
−
1
n-1
n−1个元素(
n
−
2
+
1
n-2+1
n−2+1)
. . . . . ..... ..... - 第 i − 1 i-1 i−1行: n − i + 2 n-i+2 n−i+2个元素( n − ( i − 1 ) + 1 n-(i-1)+1 n−(i−1)+1)
- 第 i i i行: j − i j-i j−i个元素
因此上三角区和主对角线元素
a
i
j
a{_{ij}}
aij(包括
a
i
j
a{_{ij}}
aij)在数组中的下标
K
=
n
+
(
n
−
1
)
+
.
.
.
+
(
n
−
i
+
2
)
+
(
j
−
i
)
=
(
i
−
1
)
×
(
2
n
−
i
+
2
)
2
+
(
j
−
i
)
K=n+(n-1)+...+(n-i+2)+(j-i)= \frac{(i-1)×(2n-i+2)}{2}+(j-i)
K=n+(n−1)+...+(n−i+2)+(j−i)=2(i−1)×(2n−i+2)+(j−i)
以上推导均假设数组的下标从0开始
4.2.3 三对角矩阵的压缩存储
对于 n n n 阶方阵 A A A中任一元素 a i j a{_{ij}} aij ,当 ∣ i − j ∣ > 1 |i-j|>1 ∣i−j∣>1时,有 a i j = 0 ( 1 ≤ i , j ≤ n ) a{_{ij}}=0 (1≤i,j≤n) aij=0(1≤i,j≤n),则称为三对角矩阵。
若已知数组下标
K
K
K,求
i
,
j
i,j
i,j(元素在第几行第几列)
4.2.4 稀疏矩阵
- 非零元素远远少于矩阵元素的个数
- 策略:顺序存储和链式存储
顺序存储——三元组<行,列,值>
4.2.5 总结
累死我了