3 栈和队列
3.1 栈
3.1.1 栈的基本概念
栈的定义
栈 (Stack)是只允许在一端 进行插入和删除操作的线性表 栈顶 (Top)。线性表允许进行插入删除的一端栈底 (Bottom)。固定的,不允许进行插入和删除的另一端空栈 。不含任何元素的空表后进先出 (Last In First Out,LIFO)栈的数学性质:n个不同元素进栈,出栈元素不同排列组合的个数为
1
n
+
1
C
2
n
n
\frac{1}{n+1}\textrm{C}_{2n}^{n}
n + 1 1 C 2 n n
栈的基本操作
InitStack(&S) :初始化一个空栈SStackEmpty(S) :判断一个栈是否为空,若栈空则返回true,否则返回falsePush(&S, x) :进栈,若栈S未满,则将x加入使之成为新栈顶Pop(&S, &x) :出栈,若栈非空,则弹出栈顶元素,并用x返回GetTop(S, &x) :读栈顶元素,若栈非空则用x返回栈顶元素DestroyStack(&S) :销毁栈,并释放栈S占用的存储空间
3.1.2 栈的顺序存储结构
顺序栈的实现
采用顺序存储的栈称为顺序栈 ,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同属附设一个指针(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.too == MaxSize-1
;栈长:S.top+1
顺序栈的基本运算
初始化
void InitStack ( SqStack & S) {
S. top = - 1 ;
}
判断空
bool StackEmpty ( SqStack S) {
if ( S. top == - 1 )
return true;
else
return false;
}
进栈
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;
}
共享栈
将两个栈的栈底分别设置在共享空间的梁端,两个栈顶像共享空间的中间延伸 栈空:0号栈top0 == -1
,1号栈top1 == MaxSize
当0号栈进栈时top0
先加1再赋值,1号栈进栈时top1
先减1再赋值;出栈时则正好相反 栈满:两个栈顶指针相邻(top1-top0 == 1
) 优点:存取时间复杂度仍为O(1),但空间利用更加有效
3.1.3 栈的链式存储结构
采用链式存储的栈称为链栈 优点:便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况 通常采用单链表实现,规定所有操作都是在单链表的表头进行的 这里规定链栈没有头结点,Lhead指向栈顶元素
typedef struct Linknode {
ElemTyoe data;
struct Linknode * next;
} * LiStack;
3.2 队列
3.2.1 队列的基本概念
队列的定义
队列 (Queue)简称队,也是一种操作受限的线性表 ,只允许在表的一端进行插入 ,而在表的另一端进行删除 向对列中插入元素的操作称为入队 或进队 ;删除元素称为出队 或离队 先进先出 (First In First Out,FIFO)
队列常见的基本操作
InitQueue(&Q) :初始化队列,构造一个空队列QQueueEmpty(Q) :判队列空,若队列Q未满,则将x加入使之成为新的队尾EnQueue(&Q, x) :入队,若队列Q未满,则将x加入使之成为新的队尾DeQueue(&Q, &x) :出队,若队列Q非空,则删除对头元素,并用x返回GetHead(Q, &x) :读队头元素,若队列Q非空则用x返回队头元素ClearQueue(&Q) :销毁队列,并释放队列Q占用的内存空间
3.2.2 队列的顺序存储结构
队列的顺序存储
采用顺序存储的队列称为顺序队 分配一块连续的存储单元存放队列中的元素,并附设两个指针:队头指针front指向队头元素,队尾指针rear指向队尾元素的下一个位置(不同定义后续操作会不同)
#define MaxSize 50
typedef struct {
ElemType data[ MaxSize] ;
int front, rear;
} SqQueue;
初始状态:Q.front == Q.rear == 0
队空条件:Q.front == Q.rear
进队操作:队不满时,先送值到队尾元素,再将队尾指针加1 出队操作:队不空时,先取队头元素值,再将队头指针加1 队列长:Q,rear - Q.front
不可用Q.rear == MaxSize
作为队列满的条件,因为可能会出现“假溢出”(Q.rear == MaxSize && Q.front != 0
)
循环队列
把存储队列元素的表从逻辑上视为一个环,称为循环队列 当队首指针Q.fornt == 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 三种处理方式区分队空和队满
牺牲一个单元来区分队空和队满(常用)
队空条件:Q.front == Q.rear
队满条件:(Q.rear + 1) % MaxSize == Q.front
队列中元素的个数:(Q.rear - Q.front + MaxSize) % MaxSize
(与队列长度一致) 类型中增设表示元素个数的数据成员
队空条件:Q.size == 0
队满条件:Q.size == MaxSize
类型中增设tag数据成员
tag == 0
时,若因删除导致Q.front == Q.rear
,则为队空tag == 1
时,若因插入导致Q.front == Q.rear
,则为队满
循环队列的操作
初始化
void InitQueue ( SqQueue & Q) {
Q. rear = Q. front = 0 ;
}
判队空
bool isEmpty ( SqQueue Q) {
if ( Q. rear == Q. front)
return true;
else
return false;
}
入队
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;
}
3.2.3 队列的链式存储结构
队列的链式存储
队列的链式表示成为链队列 是一个同时带有队头指针和队尾指针的单链表,头指针指向队头结点,尾指针指向队尾节点,即单链表的最后一个结点(与顺序存储的不同)
typedef struct {
ElemTyoe data;
struct LinkNode * next;
} LinkNode;
typedef struct {
LinkNode * front, * rear;
} LinkQueue;
链式队列的基本操作
初始化
void InitQueue ( LinkQueue & Q) {
Q. front = ( LinkNode* ) malloc ( sizeof ( LinkNode) ) ;
Q. rear = Q. front;
Q. front-> next = NULL ;
}
判队空
bool isEmpty ( LinkQueue Q) {
if ( Q. front == Q. rear)
return true;
else
return false;
}
入队
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;
}
3.2.4 双端队列
双端队列 是指允许两段都可以进行入队和出队操作的队列逻辑结构仍是线性结构 将队列的两端分别称为前端和后端,两端都可以入队和出队 在双端队列进队时,前端进的元素排列在队列中后端进元素的前面,后端进地元素排列在队列中前端进地元素的后面。在双端队列出队时,无论是前端还是后端出队,先出的元素排列在后出的元素的前面 输出受限的双端队列 :允许在一端进行插入和删除,但在另一端只允许插入的双端队列输入受限的双端队列 :允许在一端进行插入和删除,但在另一端只允许删除的双端队列若限制双端队列从某个断点插入的元素智能从该端点删除,则该双端队列就蜕变为两个栈底相邻接的栈
3.3 栈和队列的应用
3.3.1 栈在括号匹配中的应用
假设表达式中允许包含两种括号:圆括号和方括号,其嵌套的顺序任意即([]())
或[([][])]
等均为正确的,[(])
或([()]
或(()]
均为不正确的格式 算法的思想如下:
初始设置一个空栈,顺序读入括号 若是右括号,则与栈顶元素进行匹配
若匹配,则弹出栈顶元素并进行下一个元素 若不匹配,则该序列不合法 若是左括号,则压入栈中 若全部元素遍历完毕,占中非空则序列不合法
3.3.2 栈在表达式求值中的应用
处理后缀表达式 后缀表达式的运算符在操作数后面,在后缀表达式中已考虑了运算符的优先级,没有括号,只有操作数和运算符 将中缀表达式转换为后缀表达式 :
数字直接加入后缀表达式 运算符时:
若为(
,入栈; 若为)
,则依次把栈中的运算符加入后缀表达式,直到出现(
,并从栈中删除(
; 若为+
、-
、*
、/
栈空,入栈; 栈顶元素为(
,入栈; 高于栈顶元素优先级,入栈 否则,依次弹出栈顶运算符,知道一个优先级比它低的运算符或(
为止; 遍历完成,若栈非空依次弹出所有元素 通过后缀表示计算表达式值的过程为 :
顺序扫描表达式的每一项,然后根据它的类型做如下相应操作:
若该项是操作数,则将其压入栈中 若该项是操作符<op>
,则连续从栈中退出两个操作数Y和X,形成运算指令X<op>Y
,并将计算结果重新压入栈中 当表达式的所走项都扫描处理后,栈顶存放的就是最后的计算结果
3.3.3 栈在递归中的应用
若在一个函数、过程或数据结构的定义中又应用了它自身,则这个函数、过程或数据结构称为是递归定义 的,简称递归 以斐波那契数列为例,其程序实现时如下:
int Fib ( int n) {
if ( n == 0 )
return 0 ;
else if ( n == 1 )
return 1 ;
else
return Fib ( n- 1 ) + Fib ( n- 2 ) ;
}
必须注意递归模型不能是循环定义的,其必须满足下面的两个条件:
递归的精髓在于能否将原始问题转换为属性相同但规模较小的问题 递归产生的问题
在递归调用过程中,系统为每一层的返回点、局部变量、传入实参等开辟了递归工作站来进行数据存储,帝国次数过多容易造成栈溢出 通常情况下递归的效率并不高,原因是递归调用过程中包含很多重复的计算 递归转换算法转换为非递归算法,往往需要借助栈来进行
3.3.4 队列在层次遍历中的应用
在信息处理中有一大类问题需要逐层或逐行处理,这类问题的解决方法往往是在处理当前层次或当前行时就对下一层或下一行进行预处理,把处理顺序安排好,待当前层或当前行处理完毕,就可以处理下一层或下一行 使用队列是为了保存下一步的处理顺序 以二叉树层次遍历为例:
根结点入队 若队空(所有节点都处理完毕),则结束遍历;否则重复第三步操作 队列中第一个结点出队,并访问之
若其有左孩子,则将左孩子入队 若其有右孩子,则将右孩子入队 返回第二步
3.3.5 队列在计算机系统中的应用
队列在计算机系统中的应用非常广泛,以下仅从两个方面来简述队列在计算机系统中的作用:
解决主机与外部设备之间速度不匹配的问题 解决由多用户引起的资源竞争问题 例1:主机和打印机之间速度不匹配:设置一个打印数据缓冲区,其所存储的数据就是一个队列 例2:CPU资源的竞争:操作系统将每个请求在时间上的先后顺序,把它们排成一个队列。每次把CPU分配给队首请求的用户使用。当相应的程序运行结束或用完规定时间后,令其出队,再把CPU分配给新的队首请求的用户使用
3.4 特殊矩阵的压缩存储
3.4.1 数组的定义
数组 是由n(n>=1)个相同类型的数据元素构成的有限序列,每个数据元素称为一个数组元素 ,每个元素在n个线性关系中的序号称为该元素的下标 ,下标的取值范围称为数组的维界 ,并称该数组为n维数组数组与线性表的关系 :数组是线性表的推广(数组是一个逻辑结构 )。一维数组可视为一个线性表;二维数组可视为其元素也是定长线性表的线性表,以此类推数组一旦被定义,其维数和维界就不再改变 。因此,除结构的初始化和销毁外,数组只会有存取元素和修改元素 的操作
3.4.2 数组的存储结构
逻辑意义上的数组可采用计算机语言中的数组数据类型进行存储,一个数组的所有元素在内存中占用一段连续的存储空间 一维数组:
L
O
C
(
a
i
)
=
L
O
C
(
a
0
)
+
i
∗
L
LOC(a_i) = LOC(a_0) + i*L
L O C ( a i ) = L O C ( a 0 ) + i ∗ L (0 ≤ i ≤ n) 二维数组:(h2 为行下标的范围,h1 为列下标的范围)
行优先存储:
L
O
C
(
a
i
,
j
)
=
L
O
C
(
a
0
,
0
)
+
[
j
∗
(
h
2
+
1
)
+
i
]
∗
L
LOC(a_i,_j) = LOC(a_0,_0) + [j * (h_2 + 1) + i] * L
L O C ( a i , j ) = L O C ( a 0 , 0 ) + [ j ∗ ( h 2 + 1 ) + i ] ∗ L 列优先存储:
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
L O C ( a i , j ) = L O C ( a 0 , 0 ) + [ j ∗ ( h 1 + 1 ) + i ] ∗ L
3.4.3 矩阵的压缩存储
压缩存储 :指为多个值相同 的元素只分配一个 存储空间,对零元素 不分配存储空间。其目的是为了节省存储空间特殊矩阵 :指具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素 的分布有一定规律性 的矩阵。常见的特殊矩阵有对称矩阵、上(下)三角矩阵、对角矩阵等特殊矩阵的压缩存储方法 :找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布的、值相同的多个矩阵元素压缩存储到一个存储空间 中
对称矩阵
若对一个n阶方阵A[1…n][1…n]中的任意一个元素ai,j 都有ai,j =aj,i ,则称其为对称矩阵 对于一个n阶方阵,其中的元素可以划分为3个部分,即上三角区、主对角线和下三角区 对于n阶对称矩阵,上三角区的所有元素和下三角区的对应元素相同,若仍采用二维数组存放,则会浪费几乎一半的空间,为此将对称矩阵存放在一维数组B[n(n+1)/2]中,即元素ai,j 存放在bk 中。只存放下三角部分+主对角线的元素 元素下标之间的对应关系为:
k
=
{
i
(
i
−
1
)
2
+
j
−
1
,
i
≥
j
(
下
三
角
区
和
主
对
角
线
元
素
)
j
(
j
−
1
)
2
+
i
−
1
,
i
<
j
(
上
三
角
区
元
素
a
i
,
j
=
a
j
,
i
)
k=\begin{cases}\frac{i(i-1)}{2}+j-1, i\geq j(下三角区和主对角线元素)\\ \frac{j(j-1)}{2}+i-1, i<j(上三角区元素a_i,_j=a_j,_i)\end{cases}
k = { 2 i ( i − 1 ) + j − 1 , i ≥ j ( 下 三 角 区 和 主 对 角 线 元 素 ) 2 j ( j − 1 ) + i − 1 , i < j ( 上 三 角 区 元 素 a i , j = a j , i )
三角矩阵
下三角矩阵 中,上三角区的所有元素均为同一常量存储完下三角区和主对角线上的元素之后,紧接着存储对角线上方的常量 下三角矩阵元素下标之间的对应关系为:
k
=
{
i
(
i
−
1
)
2
+
j
−
1
,
i
≥
j
(
下
三
角
区
和
主
对
角
线
元
素
)
n
(
n
+
1
)
2
,
i
<
j
(
上
三
角
区
元
素
)
k=\begin{cases}\frac{i(i-1)}{2}+j-1, i\geq j(下三角区和主对角线元素)\\ \frac{n(n+1)}{2}, i<j(上三角区元素)\end{cases}
k = { 2 i ( i − 1 ) + j − 1 , i ≥ j ( 下 三 角 区 和 主 对 角 线 元 素 ) 2 n ( n + 1 ) , i < j ( 上 三 角 区 元 素 ) 上三角矩阵 中,下三角区的所有元素均为同一常量存储完上三角区和主对角线上的元素之后,紧接着存储对角线下方的常量 上三角矩阵元素下标之间的对应关系为:
k
=
{
(
i
−
1
)
(
2
n
−
i
+
2
)
2
+
(
j
−
i
)
,
i
≤
j
(
上
三
角
区
和
主
对
角
线
元
素
)
n
(
n
+
1
)
2
,
i
>
j
(
下
三
角
区
元
素
)
k=\begin{cases}\frac{(i-1)(2n-i+2)}{2}+(j-i), i\leq j(上三角区和主对角线元素)\\ \frac{n(n+1)}{2}, i>j(下三角区元素)\end{cases}
k = { 2 ( i − 1 ) ( 2 n − i + 2 ) + ( j − i ) , i ≤ j ( 上 三 角 区 和 主 对 角 线 元 素 ) 2 n ( n + 1 ) , i > j ( 下 三 角 区 元 素 )
三对角矩阵
对角矩阵也称带状矩阵 对于n阶方阵A中的任一元素
a
i
,
j
a_i,_j
a i , j ,当
∣
i
−
j
∣
>
1
\left | i-j \right |>1
∣ i − j ∣ > 1 时,有
a
i
,
j
=
0
(
1
≤
i
,
j
≤
n
)
a_i,_j = 0 (1 \leq i, j \leq n)
a i , j = 0 ( 1 ≤ i , j ≤ n ) ,则称为三对角矩阵 三对角矩阵A也可以采用压缩存储,将3条对角线上的元素按行优先方式存放在一维数组B中,且
a
1
,
1
a_1,_1
a 1 , 1 存放于
B
[
0
]
B[0]
B [ 0 ] 中 元素
a
i
,
j
a_i,_j
a i , j 在一维数组B中存放的下标为
k
=
2
i
+
j
−
3
k=2i+j-3
k = 2 i + j − 3 若已知某元素
a
i
,
j
a_i,_j
a i , j 存放在一维数组B的第k个位置,则可得
i
=
⌊
(
k
+
1
)
/
3
+
1
⌋
i = \left \lfloor (k+1)/3 + 1 \right \rfloor
i = ⌊ ( k + 1 ) / 3 + 1 ⌋ ,
j
=
k
−
2
i
+
3
j = k -2i +3
j = k − 2 i + 3
3.4.4 稀疏矩阵
矩阵中非零元素的个数t,相对矩阵元素的个数s来说非常少,即
s
≫
t
s\gg t
s ≫ t 的矩阵称为稀疏矩阵 将非零元素及其相应的行和列构成一个三元组(行标,列表,值),然后在按照某种规律存储这些三元组 注意数组下标是从0开始还是1开始 稀疏矩阵压缩存储后失去了随机存取的特性 稀疏矩阵的三元组既可以采用数组存储,也可以采用十字链表法存储