数据结构学习笔记——第3章 栈和队列

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+11C2nn
栈的基本操作
  • InitStack(&S):初始化一个空栈S
  • StackEmpty(S):判断一个栈是否为空,若栈空则返回true,否则返回false
  • Push(&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;        //指针先加1,再入栈
	return true;
}
出栈
bool Pop(SqStack &S, ElemType &x) {
	if(S.top == -1)           //栈空,报错
		return false;
	x = S.data[S.top--];      //先出栈,指针再减1
	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):初始化队列,构造一个空队列Q
  • QueueEmpty(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;           //队尾指针加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;          //队头指针加1取模
	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 LOC(ai)=LOC(a0)+iL(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 LOC(ai,j)=LOC(a0,0)+[j(h2+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 LOC(ai,j)=LOC(a0,0)+[j(h1+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={2i(i1)+j1,ij线2j(j1)+i1,i<jai,j=aj,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={2i(i1)+j1,ij线2n(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(i1)(2ni+2)+(ji),ij线2n(n+1),i>j
三对角矩阵
  • 对角矩阵也称带状矩阵
  • 对于n阶方阵A中的任一元素 a i , j a_i,_j ai,j,当 ∣ i − j ∣ > 1 \left | i-j \right |>1 ij>1时,有 a i , j = 0 ( 1 ≤ i , j ≤ n ) a_i,_j = 0 (1 \leq i, j \leq n) ai,j=0(1i,jn),则称为三对角矩阵
  • 三对角矩阵A也可以采用压缩存储,将3条对角线上的元素按行优先方式存放在一维数组B中,且 a 1 , 1 a_1,_1 a1,1存放于 B [ 0 ] B[0] B[0]
  • 元素 a i , j a_i,_j ai,j在一维数组B中存放的下标为 k = 2 i + j − 3 k=2i+j-3 k=2i+j3
  • 若已知某元素 a i , j a_i,_j ai,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=k2i+3

3.4.4 稀疏矩阵

  • 矩阵中非零元素的个数t,相对矩阵元素的个数s来说非常少,即 s ≫ t s\gg t st的矩阵称为稀疏矩阵
  • 将非零元素及其相应的行和列构成一个三元组(行标,列表,值),然后在按照某种规律存储这些三元组
  • 注意数组下标是从0开始还是1开始
  • 稀疏矩阵压缩存储后失去了随机存取的特性
  • 稀疏矩阵的三元组既可以采用数组存储,也可以采用十字链表法存储
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值