栈
概念
栈(Stack)是只允许在一端进行插入或删除操作的线性表。
首先栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作。
- 栈顶:线性表允许进行插入删除的那一端。
- 栈底:固定的,不允许进行插入和删除的另一端。
- 空栈:不含任何元素的空表。
栈的操作特性可以概括为后进先出。
性质
n个不同元素进栈,出栈元素不同排列的个数为n+11C2nn。上述公式称为卡特兰,可采用数学归纳法证明。
存储结构
顺序存储结构
顺序栈
采用顺序存储的栈称为顺序栈,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top
)指示当前栈顶元素的位置。
在这样的设计下,入栈与出栈操作就分别对应在数组尾部添加元素与删除元素,时间复杂度都为 O(1) 。
栈和队列的判空、判满条件,会因实际给的条件不同而变化,上面的代码实现只是在栈顶指针设定的条件下的相应方法,而其他情况则需具体问题具体分析。
这里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]
。相应的栈空、栈满条件也会发生变化。
共享栈
利用栈底位置相对不变的特性,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。
两个栈的栈顶指针都指向栈顶元素,top0 =- 1
时0号栈为空,top1 = MaxSize
时1号栈为空;仅当两个栈顶指针相邻top1 - top0 = 1
时,判断为栈满。当0号栈进栈时top0
先加1再赋值,1号栈进栈时top1
先减1再赋值;出栈时则刚好相反。
共享栈是为了更有效地利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢。
其存取数据的时间复杂度均为0(1),对存取效率没有什么影响。
链式存储结构
采用链式存储的栈称为链栈,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。
通常采用单链表实现,并规定所有操作都是在单链表的表头进行的。
采用链式存储,便于结点的插入与删除。链栈的操作与链表类似,入栈和出栈的操作都在链表的表头进行。
将链表头部作为栈顶的一端,可以避免在实现数据 “入栈” 和 “出栈” 操作时做大量遍历链表的耗时操作。
对于带头结点和不带头结点的链栈,具体的实现会有所不同。
队列
概念
队列(Queue)简称队,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。
-
向队列中插入元素称为入队或进队;
-
删除元素称为出队或离队。
这和我们日常生活中的排队是一致的,最早排队的也是最早离队的,其操作的特性是先进先出。
InitQueue (&Q)
:初始化队列,构造一个空队列Q。
QueueEmpty (Q)
:判队列空,若队列Q为空返回true,否则返回false。
EnQueue (&Q, x)
:入队,若队列Q未满,将x加入,使之成为新的队尾。
DeQueue (&Q, &x)
:出队,若队列Q非空,删除队头元素,并用x返回。
GetHead(Q,&x)
:读队头元素,若队列Q非空,则将队头元素赋值给X。
存储结构
顺序存储结构
顺序存储
队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针:
- 队头指针
front
指向队头元素
- 队尾指针
rear
指向队尾元素的下一个位置
队列的顺序存储类型可描述为
- 初始化:
Q.front=Q.rear=0
- 进队:队不满时,先送值到队尾元素,再将队尾指针加1
- 出队:队不空时,先取队头元素值,再将队头指针加1
Q. rear==MaxSize
不能作为队列满的条件。下图(d)中,队列中仅有一个元素,但仍满足该条件。这时入队出现“上溢出”,但这种溢出并不是真正的溢出,在 data数组中依然存在可以存放元素的空位置,所以是一种“假溢出”。为了解决这个缺点可以使用循环队列。
循环队列
将顺序队列臆造为一个环状的空间, 即把存储队列元素的表从逻辑上视为一个环,称为循环队列。
当队首指针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
-
判断队满:
-
牺牲一个单元来区分队空和队满,此时
队满条件:(Q.rear+1) %MaxSize == Q.front
队空条件:Q.front == Q.rear
-
类型中增设一个数据成员,用来表示元素个数,此时
队空条件:Q.size == 0
队满条件: Q.size == MaxSize
-
类型中增设tag
数据成员,用来表示最近进行的操作。删除操作成功时,tag = 0
;插入操作成功时,tag = 1
。此时,
队空条件:Q.front == Q.rear && tag = 0
队满条件:Q.front == Q.rear && tag = 1
以下代码是以牺牲一个单元来区分队空和队满
队列的链式存储结构
队列的链式表示称为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表。
头指针指向队头结点,尾指针指向队尾结点,即单链表的最后一个结点。
双端队列
双端队列是指允许两端都可以进行入队和出队操作的队列。其元素的逻辑结构仍是线性结构。
由此定义还可以衍生出:
- 输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入的双端队列
- 输入受限的双端队列:允许在一端进行插入和删除,但在另一端只允许删除的双端队列
栈和队列的应用
栈在括号匹配中的应用
栈在表达式求值中的应用
https://www.bilibili.com/video/BV1b7411N798?p=29&vd_source=18ba17bb87b1c219c021b5fd3be34461
中缀表达式 | 前缀表达式(波兰表达式) | 后缀表达式(逆波兰表达式) |
---|
a+b | ab+ | +ab |
a+b-c | ab+c- | -+abc |
a+b-c*d | ab+cd*- | -+ab*cd |
栈在递归中的应用
递归是一种重要的程序设计方法。简单地说,若在一个函数、过程或数据结构的定义中又应用了它自身,则这个函数、过程或数据结构称为是递归定义的,简称递归。
递归模型不能是循环定义的,其必须满足下面的两个条件: 递归表达式(递归体)、边界条件(递归出口)。
队列的应用
https://www.bilibili.com/video/BV1b7411N798/?p=31
树的层次遍历和图的广度优先遍历。
队列在计算机系统中的作用:
-
解决主机与外部设备之间速度不匹配的问题
以主机和打印机之间速度不匹配的问题为例。有一个打印数据缓冲区,主机把要打印输出的数据依次写入这个缓冲区,写满后就暂停输出,转去做其他的事情。打印机就从缓冲区中按照 先进先出的原则依次取出数据并打印,打印完后再向主机发出请求。主机接到请求后再向缓冲区 写入打印数据。
-
解决由多用户引起的资源竞争问题
CPU资源的竞争就是一个典型的例子。
特殊矩阵的压缩存储
https://www.bilibili.com/video/BV1b7411N798/?p=32
- 压缩存储:指为多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。其目的是节省存储空间。
- 特殊矩阵:指具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布有一定规律性的矩阵。常见的特殊矩阵有对称矩阵、上(下)三角矩阵、对角矩阵等。
- 特殊矩阵的压缩存储方法:找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布的、值相同的多个矩阵元素压缩存储到一个存储空间中。
对称矩阵
二维数组 A[n][n]
和 A[0...n-1] [O...n-1]
的写法是等价的。
如果数组写为 A[1...n] [1...n]
,则说明指定了从下标1开始存储元素。
二维数组元素写为a[i][j]
,注意数组 元素下标 i 和 j 通常是从0开始的。
矩阵元素通常写为 ai,j 或 a(i)(j),注意行号 i 和列号 j $是 从1开始的。
三角矩阵
三对角矩阵
稀疏矩阵
若采用常规的方法存储稀疏矩阵,则相当浪费存储空间,因此仅存储非零元素。但通常非零元素的分布没有规律,所以仅存储非零元素的值是不够的,还要存储它所在的行和列。因此,将非零元素及其相应的行和列构成一个三元组。然后按照某种规律存储这些三元组线性表。
稀疏矩阵压缩存储后便失去了随机存取特性。
三元组仅存储行,列,值是不够的,因为无法通过这些判断稀疏矩阵的大小。因此还需要保存稀疏矩阵的行数和列数。
稀疏矩阵的三元组表既可以采用数组存储,又可以采用十字链表存储。当存储稀疏矩阵时,不仅要保存三元组表,而且要保存稀疏矩阵的行数、列数和非零元素的个数。