第三章 栈和队列
【内容】
(一)栈和队列的基本概念
(二)栈和队列的顺序存储结构
(三)栈和队列的链式存储结构
(四)栈和队列的应用
(五)特殊矩阵的压缩存储
【知识框架】
3.1 栈的定义
3.1.1 栈的基本概念
- 栈的定义
栈(stack)是只允许在一端进行插入和删除操作的线性表。
首先栈是一种线性表,限定这种线性表只能在某一端进行插入和删除操作。
栈顶(Top):线性表允许进行插入删除的那一端。
栈底(Bottom):固定的,不允许进行插入和删除的一端。
空栈:不含任何元素的空表。
栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。也就是说,栈底是固定的,最先进栈的只能在栈底。
注:栈首先是一个线性表,也就是说栈元素具有线性关系,即前驱后继关系。只不过是一种特殊的线性表,
栈的定义是说在线性表的表尾进行插入和删除操作,这里表尾是指栈顶,而不是栈底。
- 栈的基本操作(抽象数据类型)
InitStack(& S) | 初始化一个空栈S |
---|---|
StackEmpty(S) | 判断一个栈是否为空,若栈S为空则返回True,否则返回False |
Push(&S,x) | 进栈,若栈S未满,则将x加入使之成为新栈顶 |
Pop(&S,&x) | 出栈,若栈S非空,则弹出栈顶元素,并用x返回 |
GetTop(S,&x) | 读栈顶元素,若栈S非空,则用x返回栈顶元素 |
DestoryStack(&S) | 栈销毁,并释放栈S占用的存储空间(& 表示引用调用) |
3.1.2 栈的顺序存储结构
- 顺序栈的实现
采用顺序存储的栈称为顺序栈
。
它利用一组抵制连续的存储单元存放自栈底带栈顶的数据元素,同时附设一个指针(top)指示当前栈顶元素的位置。
栈的结构定义
# define MaxSize 100
typedef struct{
ElemType data[MaxSize];
int top;
}SqStack;
栈顶指针:S.top,初始时设置S.top==-1;栈顶元素S.data[S.top];
进栈操作:栈不满时,栈顶指针先加1,再送值到栈顶元素。
出栈操作:栈非空时,先取栈顶元素值,再将栈顶指针减1。
栈空条件:S.top==-1;
栈满条件:S.top==MaxSize-1;
栈长:S.top+1;
假设有个栈,StackSize为5,普通栈,空栈,栈满情况如下:
- 顺序栈的基本操作
顺序栈常用基本运算:
(1)初始化
void InitStack(SqStack &S){
S.top=-1;
}
(2)判空栈
bool StackEmpty(SqStack &S){
if(S.top==-1)
return true;
else
retuen false;
}
(3)进栈
bool Push(SqStack &S,ElemType x){
if(S.top=MaxSize-1)
return false;
S.data[++S.top]=x; //指针先加1,再入栈
return true;
}
栈不满时,top先加1,再入栈。
(4)出栈
bool Pop(SqStack &S,ElemType &x){
if(S.top==-1)
return false;
x=S.data[S.top--];
return true;
}
(5)读取栈顶元素
bool GetTop(SqStack S,ElemType &x){
if(S.top==-1)
return false;
x.data[S.top];
return true;
}
仅为读取栈顶元素,并没有出栈操作,因此原栈顶元素依然保留在栈中。
注意: 这里 top 指向的是栈顶元素,所以进栈操作
为 S.data[++S.top]=x
, 出栈操作
为x=S.data[S.top--]
。
若栈顶指针初始化
为S.top=0
,即top指向栈顶元素的下一位置,则入栈操作
变为S.data[S.top++]=x;
出栈操作
变为 x=S.data[--S.top]
。
相应的栈空、栈满条件也会发生变化。
- 共享栈
数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,即下标为 0 处,另一个栈为栈的末端,即下标为数组长度 n-1处。
这样,两个栈如果增加元素,就是两端点向中间延伸。
其实关键思路是:它们是在数组的两端,向中间靠笼。top1 和 top2 是栈 1 和栈 2 的栈顶指针,可以想象,只要它们俠不见面,两个栈就可以一直使用。
从这里也就可以分析出来,栈 1 为空时,就是 top1 等于一1 时; 而当 top2 等于 n时,即是栈 2 为空时,那什么时候栈满?
极端的情况,若栈 2 是空栈,栈 1 的 top 1 等于 n-1时,就是栈 1 满了。
反之,当栈 1 为空栈时,top2 等于 0 时,为栈 2 满。
但更多的情况,两个栈见面之时,也就是两个指针之间相差 1 时,即 top1+1==top 2 为栈满。
3.1.3 栈的链式存储结构
栈的链式存储结构简称为链栈
。
对于链栈来说,是不需要头结点的,也不存在栈满的情况,除非是内存已经没有可以使用的空间。
对空栈来说,链表原定义是头指针指向空,链栈的空就是top=NULL的时候。
链栈结构代码:
typedef struct StackNode{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStackPtr;
typedef struct LinkStack{
LinkStackPtr top;
int count;
}LinkStack;
- 进栈操作
链栈的进栈Push操作,假设元素值为e的新结点是s,top为栈顶指针:
Status Push(LinkStack *S,SElemType e){
LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
s->data=e;
s->next=S->top;
S->top=s;
S->count++;
return OK;
}
- 出栈操作
假设变量p用来存储要删除的栈顶顶点,将栈顶指针下移一位,最后释放p即可。
Status Pop(LinkStack *S,SElemType *e){ LinkStackPtr p; if(StackEmpty(*S)) return ERROR; *e=S->top->data; p=S->top; S->top=S->top->next; free(p); S->count--; return OK;}
进栈和出栈的时间复杂度均为O(1)。
在栈的使用过程中元素变化不可预料,最好用链栈,反之,变化在可控范围内,使用顺序栈会好一些。
3.1.4 栈的应用-斐波那契数列
F ( n ) = { 0 , 当 n = 0 1 , 当 n = 1 F ( n − 1 ) + F ( n − 2 ) , 当 n > 1 F(n)=\left\{\begin{array}{l} 0, \quad \text { 当 } n=0 \\ 1, \text { 当 } n=1 \\ F(n-1)+F(n-2), \quad \text { 当 } n>1 \end{array}\right. F(n)=⎩ ⎨ ⎧0, 当 n=01, 当 n=1F(n−1)+F(n−2), 当 n>1
//打印前40位斐波那契数列int main(){ int i; int a[40]; a[0]=0; a[1]=1; printf("%d",a[0]); printf("%d",a[1]); for(i=2;i<40;i++){ a[i]=a[i-1]=a[i-2]; printf("%d",a[i]); } return 0;}
//斐波那契数列递归实现int Fbi(int i){ if(i<2) return i==0?0:1; return Fbi(i-1)+Fbi(i-2);}int main(){ int i; for(int i=0;i<40;i++) printf("%d",fbi(i)); return 0;}
递归定义:
把一个直接调用自己活着通过一系列的调用语句间接地调用自己的函数,称作递归函数。
每个递归定义必须至少有一个条件,满足时递归不再进行,即不饮用自身而是返回值退出。
3.1.5 栈在括号匹配中的应用
假设表达式中允许包含两种括号: 圆括号和方括号,其嵌套的顺序任意即([] ())或[([][])]等均 为正确的格式,[(])或([O)或(O]均为不正确的格式。 考虑下列括号序列:
分析如下:
1 ) 计算机接收第 1 个括号“ [“后,期待与之匹配的第 8 个括号 “]”出现。
2) 获得了第 2 个括号“(”,此时第 1 个括号“ [ “暂时放在一边,而急迫期待与之匹配的第 7 个括号 “)“出现。
3) 获得了第 3 个括号”[”,此时第 2 个括号 “ (" 暂时放在一边,而急迫期待与之匹配的第 4 个括号“]”出现。第 3 个括号的期待得到满足,消解之后,第 2 个括号的期待匹配又 成为当前最急迫的任务。
4)以此类推,可见该处理过程与栈的思想吻合。 算法的思想如下:
1)初始设置一个空栈,顺序读入括号。
2)若是右括号,则或者使置于栈顶的最急迫期待得以消解,或者是不合法的情况(括号序 列不匹配,退出程序)。
3) 若是左括号,则作为一个新的更急迫的期待压入栈中,自然使原有的在栈中的所有未消 解的期待的急迫性降了一级。算法结束时,栈为空,否则括号序列不匹配。
3.2 队列
3.2.1 队列的基本概念
- 队列的定义
**队列(queue)**是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)
的线性表
,简称FIFO
。
允许插入的一端称为队尾
,允许删除的一端称为队头
。
空队列
:不含任何元素的空表
- 队列的抽象数据类型
InitQueue(*Q) | 初始化操作,建立一个空队列Q |
---|---|
DestoryQueue(*Q) | 若队列Q存在,则销毁它 |
ClearQueue(*Q) | 将队列Q清空 |
QueueEmpty(Q) | 若队列Q为空,返回true,否则返回false |
GetHead(Q,*e) | 若队列Q存在且非空,用e返回队列Q的队头元素 |
EnQueue(*Q,e) | 若队列Q存在,插入新元素e到队列Q中并成为队尾元素 |
DeQueue(*Q,*e) | 删除队列Q中对头元素,并用e返回其值 |
QueueLength(Q) | 返回队列Q的元素个数 |
3.2.2 队列的顺序存储结构
- 队列的顺序存储
队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针:队头指针 front
指向队头元素, 队尾指针 rear
指向队尾元素的下一个位置。
队列的顺序存储类型:
#define MaxSize 50typedef struct{ ElemType data[MaxSize]; int front,rear;}SqQueue;
初始状态(队空条件):Q.frontQ.rear0.
进队操作:队不满时,先送值到队尾元素,再将队尾指针加1。
出队操作:队不空时,先取队头元素值,再将队头指针加1。
顺序队列缺点:
图 (a)所示为队列的初始状态,有 Q. frontQ.rear0 成立,该条件可以作为队列判 空的条件。
但不能否用 Q.rear==Maxsize 作为队列满的条件,(d)中,队列中 仅有一个元素,但仍满足该条件。这时入队出现“上溢出”,但这种溢出并不是真正的溢出,在 data 数组中依然存在可以存放元素的空位置,所以是一种“假溢出”。
- 循环队列
把存储队列的表从逻辑上视为一个环,这种头尾相接的顺序存储结构称为循环队列
。
初始时:Q.feont=Q.rear=0。
队首指针进1:Q.front=(Q.front+1)%MaxSize。
队尾指针进1:Q.rear=(Q.rear+1)%MaxSize。
出队入队时:指针都按顺时针方向进1。
那么,循环队列队空和队满的判断条件是: Q. front==Q.rear。
若入队元素的速度快于出队元素的速度,则队尾指针很快就会赶上队首指针,如图 d(1)所示, 此时可以看出队满时也有 Q.front==Q.rear。循环队列出入队示意图如图 所示。
为了区分对空还是队满,三种处理方式:
- 牺牲一个单元来区分队空和队满,入队时少用一个队列单元,这是一种较为普遍的做法, 约定以“队头指针在队尾指针的下一位置作为队满的标志”,如图d(2)所示。
队满条件
: (Q.rear+1) %MaxSizeQ.front。
队空条件仍
: Q.frontQ.rear。
队列中元素的个数
: (Q.rear-Q.front+MaxSize) % MaxSize。
2)类型中增设表示元素个数的数据成员。
队空的条件为 Q .size==0 ;
队满的条件为:Q.size==MaxSize。
这两种情况都有 Q .$ front==Q.rear。
- 类型中增设 tag 数据成员,以区分是队满还是队空。tag 等于 0 时,若因删除导致
Q. front ==Q . rear, 则为队空;
tag 等于 1 时,若因插入导致 Q. front== Q . rear, 则为队满。
- 循环队列的操作
(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; 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; return true;}
3.2.3 队列的链式存储结构
- 队列的链式存储
队列的链式表示称为链队列,实际上是一个同时带有队头指针和队尾指针的单链表,头指针指向队头结点,尾指针指向队尾结点,即单链表的最后一个结点,即尾进头出。
空队列时,front和rear都指向头结点
队列的链式存储类型:
typedef struct{ ElemType data; struct LinkNode *next;}LinkNode;typedef struct{ LinkNode *front,*rear;}LinkQueue;
当Q.frontNULL且Q.rearNULL时,链队列为空。
- 链式队列基本操作
(1)初始化
void IniQueue(LinkQueue &Q){ Q.front=Q.rear=(LinkNode *)malloc(sizeof(LinkNode)); Q.front->next=NULL;}
(2)判空队
bool IsEmpty(LinkQueue Q){ if(q.front==Q.rear) return true; else return false;}
(3)入队
入队时, 建立一个新结点,将新结点插入到链表的尾部,并改让 Q.rear 指向这个新插入的结点(若原队 列为空队,则令Q .front 也指向该结点)。
void EnQueue(LinkQueue &Q,ElemType &x){ LinkNode *s=(LinkNode *)malloc(sizeof(LNode)); s->data=x; s->next=NULL; Q.rear->next=s; //拥有元素e新结点s赋值给原队尾结点的后继-1 Q.rear=s; //把当前的s设置为队尾结点,rear指向s-2}
(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; //欲删除的队头结点暂存给p--1 x=p->data; //欲删除的队头结点的值赋值给x Q.front->next=p->next; //原队头结点后继p->next赋值给头结点后继---2 if(Q.rear==p) Q.rear=Q.front; //原队列只有一个结点,删除后变空 free(p); return true;}
3.2.4 双端队列
双端队列是指允许两端都可以进行入队和出队操作的队列,如图所示。其元素的逻辑结 构仍是线性结构。
将队列的两端分别称为前端
和后端
,两端都可以入队和出队。
在双端队列进队时,前端进的元素排列在队列中后端进的元素的前面,后端进的元素排列在队列中前端进的元素的后面。在双端队列出队时,无论是前端还是后端出队,先出的元素排列在后出的元素的前面。
输山受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入的双端队列称为输出受限的双端队列。
输入受限的双端队列:允许在一端进行插入和删除,但在另一端只允许删除的双端队列称为输入受限的双端队列。
若限定双端队列从某个端点插入的元素只能从该端点删除, 则该双端队列就阅变为两个栈底相邻接的栈。
3.4 特殊矩阵的压缩存储
3.4.1 数组的定义
数组是由 n(n>1)个相同类型的数据元素构成的有限序列
,每个数据元素称为一个数组元素,每个元素在 n 个线性关系中的序号称为该元素的下标
,下标的取值范围称为数组的维界
。
数组与线性表的关系:数组是线性表的推广。
一维数组可视为一个线性表; 二维数组可视为 其元素也是定长线性表的线性表,以此类推。
数组一旦被定义,其维数和维界就不再改变。因此, 除结构的初始化和销毁外,数组只会有存取元素和修改元素的操作。
3.4.2 数组的存储结构
以一维数组A[0…n-1]为例,其存储结构关系式为
LOC
(
a
i
)
=
LOC
(
a
0
)
+
i
×
L
(
0
≤
i
<
n
)
\operatorname{LOC}\left(a_{i}\right)=\operatorname{LOC}\left(a_{0}\right)+i \times L(0 \leq i<n)
LOC(ai)=LOC(a0)+i×L(0≤i<n)
其中,L 是每个数组元素所占的存储单元。
对于多维数组,有两种映射方法:按行优先和按列优先。以二维数组为例,按行优先存储的 基本思想是:先行后列,先存储行号较小的元素,行号相等先存储列号较小的元素。设二维数组 的行下标与列下标的范围分别为[0.h1]与 [0.h2]则存储结构关系式为
LOC
(
a
i
,
j
)
=
LOC
(
a
0
,
0
)
+
[
i
×
(
h
2
+
1
)
+
j
]
×
L
\operatorname{LOC}\left(a_{i, j}\right)=\operatorname{LOC}\left(a_{0,0}\right)+\left[i \times\left(h_{2}+1\right)+j\right] \times L
LOC(ai,j)=LOC(a0,0)+[i×(h2+1)+j]×L
例:对于数组A2*3,按行优先方式在内存中存储形式:
当以列优先方式存储时,得出存储结构关系式为
LOC
(
a
i
,
j
)
=
LOC
(
a
0
,
0
)
+
[
j
×
(
h
1
+
1
)
+
i
]
×
L
\operatorname{LOC}\left(a_{i, j}\right)=\operatorname{LOC}\left(a_{0,0}\right)+\left[j \times\left(h_{1}+1\right)+i\right] \times L
LOC(ai,j)=LOC(a0,0)+[j×(h1+1)+i]×L
例:对于数组A2*3,按列优先方式在内存中存储形式:
3.4.3 矩阵的压缩存储
压缩存储: 指为多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。其目的 是为了节省存储空间。
特殊矩阵: 指具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布有一 定规律性的矩阵。
常见的特殊矩阵有对称矩阵、上(下)三角矩阵、对角矩阵等。
特殊矩阵的压缩存储方法:找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布的、值相同的多个矩阵元素压缩存储到一个存储空间中。
- 对称矩阵
若对一个 n 阶方阵 A [ 1 . . . m] 中的任意一个元素ai,j 都有 ai,j =aj,i, 则称其为对称矩阵。
对于一个n阶方阵,其中的元素可以划分为 3 个部分, 即上三角区、主对角线和下三角区,如图 所示。
对于n阶对称矩阵,上三角区的所有元素和下三角区的对应元素相同,若仍采用二维数组存放,则会浪费几乎一半的空间,为此将对称矩阵 A [1…n ] [ 1…n] 存放 在一维数组 B [n (n+1) /2] 中,即元素ai,j 存放在 bk中。只存放下三角部分(含主对角)的元素。
因此,元素ai,j在数组B 中的下标 k=1+2+…+(i-1)+j-1=i(i-1) / 2+j-1数组下标从0开始)。因此,元素下标之间的对应关系如下:
k
=
{
i
(
i
−
1
)
2
+
j
−
1
,
i
≥
j
(
下三角区和主对角线元素
)
j
(
j
−
1
)
2
+
i
−
1
,
i
<
j
(
上三角区元素
a
i
j
=
a
j
i
)
k=\left\{\begin{array}{ll} \frac{i(i-1)}{2}+j-1, & i \geq j(\text { 下三角区和主对角线元素 }) \\ \frac{j(j-1)}{2}+i-1, & i<j\left(\text { 上三角区元素 } a_{i j}=a_{j i}\right) \end{array}\right.
k={2i(i−1)+j−1,2j(j−1)+i−1,i≥j( 下三角区和主对角线元素 )i<j( 上三角区元素 aij=aji)
- 三角矩阵
下三角矩阵 [见图 (a)] 中,上三角区的所有元素均为同一常量。
其存储思想与对称矩阵类似,不同之处在于存储完下三角区和主对角线上的元素之后,紧接着存储对角线上方的常量一 次,故可以将下三角矩阵 A[1…n] [1…n]压缩存储在B[n(n+1)/2+1]中。 元素下标之间的对应关系为
k
=
{
i
(
i
−
1
)
2
+
j
−
1
,
i
≥
j
(
下三角区和主对角线元素
)
n
(
n
+
1
)
2
,
i
<
j
(
上三角区元素
)
k=\left\{\begin{array}{ll} \frac{i(i-1)}{2}+j-1, & i \geq j(\text { 下三角区和主对角线元素 }) \\ \frac{n(n+1)}{2}, & i<j(\text { 上三角区元素 }) \end{array}\right.
k={2i(i−1)+j−1,2n(n+1),i≥j( 下三角区和主对角线元素 )i<j( 上三角区元素 )
下三角矩阵在内存中的压缩存储形式:
上三角矩阵 [见图(b)] 中,下三角区的所有元 素均为同一常量。只需存储主对角线、上三角区上的元素和下三角区的常量一次,可将其压缩存储在 B [n (n+1) / 2+1] 中。
因此,元素ai,j在数组B中的下标k=n+(n-1)+…+(n-i+2)+(j-i+1)-1=(i-1)(2 n-i+2) / 2+(j-i)。
因此,元素下标之间的对应关系如下:
k
=
{
(
i
−
1
)
(
2
n
−
i
+
2
)
2
+
(
j
−
i
)
,
i
≤
j
(
上三角区和主对角线元素
)
n
(
n
+
1
)
2
,
i
>
j
(
下三角区元素
)
k=\left\{\begin{array}{ll} \frac{(i-1)(2 n-i+2)}{2}+(j-i), & i \leq j(\text { 上三角区和主对角线元素 }) \\ \frac{n(n+1)}{2}, & i>j(\text { 下三角区元素 }) \end{array}\right.
k={2(i−1)(2n−i+2)+(j−i),2n(n+1),i≤j( 上三角区和主对角线元素 )i>j( 下三角区元素 )
上三角矩阵在内存中的压缩存储形式:
- 三角对称矩阵
对角矩阵也称带状矩阵。对于n阶方阵 A 中的任一元素ai,j, 当 |i-j|>1 时,有 ai,j=0 ( 1 <=i, j <=n), 则称为三对角矩阵,如图所示。
在三对角矩阵中,所有非零元素都集中在以主对角线为中心的3条对角线的区域,其他区域的元素都为零。
三对角矩阵A也可以采用压缩存储,将3条对角线上的元素按行优先方式存放在一维数组B中,且a1,1存放于B[0]中,其存储形式如图所示。
由此可以计算矩阵A中3条对角线上的元素ai,j(1<=i,j<=n,|i-j|<=1)在一维数组B中存放的下标为k=2i+j-3。
3.4.4 稀疏矩阵
矩阵中非零元素的个数t,相对矩阵元素的个数s来说非常少,即s>>t的矩阵称为稀疏矩阵
。
例如,一个矩阵的阶为100x100,该矩阵中只有少于100个非零元素。
若采用常规的方法存储稀疏矩阵,则相当浪费存储空间,因此仅存储非零元素。但通常零元素的分布没有规律,所以仅存储非零元素的值是不够的,还要存储它所在的行和列。因此,将非零元素及其相应的行和列构成一个三元组(行标,列标,值),如图所示。然后再按照某种规律存储这些三元组。稀疏矩阵压缩存储后便失去了随机存取特性。
稀疏矩阵的三元组既可以采用数组存储,也可以采用十字链法存储。