第3章:栈、队列和数组

372 阅读14分钟

1、栈

1.1、栈的基本概念

概述

栈是只允许在一端进行插入或删除操作的线性表(先进后出:LIFO)

栈顶(Top):线性表允许进行插入和删除的那一端

栈底(Bottom):固定的,不允许进行插入和删除的那一端

空栈:不含任何元素的空表

栈的基本操作

  • 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 占用的空间

卡特兰数公式

Catalan(n)=(2n)!(n+1)!n!=1n+1C2nnCatalan(n)=\frac{(2n)!}{(n+1)!n!}=\frac{1}{n+1}C_{2n}^n

1.2、栈的顺序存储结构

采用顺序存储的栈称为顺序栈,他利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指示当前栈顶元素的位置

#define MaxSize 50	//定义栈中元素的最大个数
typedef struct{
    ElemType data[MaxSize];	//存放栈中元素
    int top;				//栈顶指针
}SqStack;

栈顶指针:S.top,初始时设置 S.top = -1
栈顶元素:S.data
入栈操作:栈不满时,栈顶指针先加1,再送值到栈顶
出栈操作:栈非空时,先取栈顶元素,再将栈顶指针减1
栈空条件:S.top = -1;栈满条件:S.top = 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 GetElem(SqStack S,ElemType &x){
    if(S.top == -1)		//栈空,报错
        return false;
    x = S.data[S.top];	//x记录栈顶元素
    return true;
}

共享栈

利用栈底位置相对不变性,可以让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。这样可以在解决上溢的基础上节省存储空间

top0=-1:0 号栈为空
top1=MaxSize:1 号栈为空

top1-top0=1:栈满

1.3、栈的链式存储结构

采用链式存储结构的栈称为链栈,链栈的优点是便于多个栈共享存储空间和提高效率,且不存在栈满上溢的情况。通常采用单链表实现链栈,并规定所有操作都是在单链表表头进行,没有头结点,Lhead 指向栈顶元素

typedef struct Linknode{
    ElemType data;	//数据域
    struct Linknode *next;	//指针域
}LiStack	//栈类型定义

2、队列

2.1、队列的基本概念

概述

队列是一种操作受限的线性表,只允许在表的一端插入,而在表的另一端删除。向队列插入元素称为入队或进队,删除元素称为出队或离队(先进先出:FIFO)

队头:允许删除的一端,也称对首

队尾:允许插入的一端

空队列:不含任何元素的空表

队列的基本操作

  • InitQueue(&Q):初始化队列,构造一个空队列 Q
  • QueueEmpty(Q):判队列空,若队列 Q 为空返回 true,否则返回 false
  • EnQueue(&Q,x):入队,若队列 Q 未满,将 x 加入,使之成为新的队尾
  • DeQueue(&Q,&x):出队,若队列 Q 非空,删除队首元素,并用 x 返回
  • GetHead(Q,&x):读队首元素,若队列非空,则将队首元素赋值给 x

2.2、队列的顺序存储结构

队列的顺序存储

指分配一块连续的存储单元存放队列中的元素,并附设两个指针:队首指针 front 指向队首元素,队尾指针 rear 指向队尾元素(队尾元素下一个空位)

#define MaxSize 50	//定义队列中元素的最大个数
typedef struct{
    ElemType data[MaxSize];	//用数组存放队列元素
    int front,rear;		//队首指针和队尾指针
}

初始时:Q.front = Q.rear = 0
入队操作:队不满时,先送指到队尾元素,再将队尾指针加 1
出队操作:队不空时,先取队首元素值,再将队首指针加 1

循环队列

单纯使用顺序队列会出现“假溢出”问题(因为入队,出队指针都是在加,而分配的数组空间有限,最终 front 与 rear 都等于 MaxSize,但数组是空的)。于是将顺序队列逻辑上视为一个环,称为循环队列。当队首指针Q.front=MaxSize1Q.front=MaxSize-1后,再前进一个位置自动到数组下标为 0 的位置,这可以用除法取模运算实现

初始时:Q.front=Q.rear=0Q.front = Q.rear = 0

队首指针进 1:Q.front=(Q.front+1)%MaxSizeQ.front = (Q.front+1)\%MaxSize

队尾指针进 1:Q.rear=(Q.rear+1)%MaxSizeQ.rear= (Q.rear+1)\%MaxSize

队列长度:(Q.rear+MaxSizeQ.front)%MaxSize(Q.rear+MaxSize-Q.front)\%MaxSize

出入队时:指针都按顺时针方向进 1

循环队列判空/满

方式1、牺牲一个单元来区分队空或队满,入队时少用一个队列单元

约定以“队首指针在队尾指针的下一个位置作为队满的标志”

队满条件:(Q.rear+1)%MaxSize==Q.front(Q.rear+1)\%MaxSize==Q.front

队空条件:Q.front==Q.rearQ.front==Q.rear

队列中元素个数:(Q.rear+MaxSizeQ.front)%MaxSize(Q.rear+MaxSize-Q.front)\%MaxSize

方式2、类型中增设 size 数据成员,表示元素个数

删除成功 size-1;插入成功 size+1

队空:Q.size==0Q.size==0

队满:Q.size==MaxSizeQ.size==MaxSize

队空队满都有可能Q.front==Q.rearQ.front==Q.rear

方式3、类型中增设 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.front == Q.rear)	//队空条件
        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 false;
}

2.3、队列的链式存储结构

概述

队列的链式表示称为链式队列,实际上是同时有队首指针和队尾指针的单链表。队首指针指向对头结点;队尾指针指向队尾结点

typedef struct LinkNode{	//链式队列结点
    ElemType data;
    struct LinkNode *next;
}LinkNode;

typedef struct{		//链式队列
    LinkNode *front, *rear; //队列的队头和队尾指针
}LinkQueue;

链式队列判空

通常将链式队列设计成一个带头结点的单链表,以便统一插入和删除操作

//初始化
void InitQueue(LinkQueue &Q){
    Q.front=Q.rear=(LinkNode *)malloc(sizeof(LinkNode)); //建立头结点
    Q.front->next = NULL;   //初始为空
}
//判空
bool IsEmpty(LinkQueue Q){
    if(Q.front == Q.rear)	//判空条件
        return true;
    else
        return false;
}
//入队
bool 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(LinkNode &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(q);
    return true;
}

2.4、双端队列

双端队列:指允许两端都可以进行插入和删除操作的线性表,通常将左端视为前端,右端视为后端

操作受限的双端队列

  • 输出受限:只允许在一端插入和删除,在另一端只允许删除
  • 输入受限:只允许在一端插入和删除,在另一端只允许插入

3、栈和队列的应用

3.1、栈在括号匹配中的应用

算法思想

  1. 初始设置一个空栈,顺序读入括号(压栈)
  2. 如果待入栈括号类型与栈顶括号类型相匹配(一左一右)则两者相消,如果不同,则括号压栈,继续判断下一个括号
  3. 算法结束时,栈为空,则括号匹配;栈不为空,则括号不匹配

3.2、栈在表达式求值中的应用

算数表达式
  1. 中缀表达式
  • 运算符位于两个操作数之间的表达式表示法
  • 需要使用括号来明确运算顺序
  • 需要定义运算符优先级
  • A+(BC)D/EA+(B*C)-D/E
  1. 前缀表达式(波兰表示法)
  • 运算符位于其操作数之前的表达式表示法
  • 不需要括号即可明确运算顺序
  • 运算符优先级隐含在表达式的结构中
  • +ABC/DE- + A * B C / D E等价于中缀的A+(BC)D/EA +(B*C)-D/E
  1. 后缀表达式(逆波兰表达式)
  • 运算符位于其操作数之后的表达式表示法
  • 同样不需要括号即可明确运算顺序
  • 计算时可以使用栈结构高效处理
  • ABC+DE/A B C * + D E / -等价于中缀的A+(BC)D/EA+(B * C) - D / E
中缀表达式转后缀表达式
  1. 初始化
  • 创建一个空栈(用于存放运算符)
  • 创建一个空输出列表(存放后缀表达式)
  1. 从左到右扫描中缀表达式
  • 遇到操作数:直接加入输出列表

  • 遇到左括号:压入栈中

  • 遇到右括号:弹出栈顶元素并加入输出列表,直到相匹配的左括号并弹出

  • 遇到运算符(同优先级左结合除了 ^)

    • 如果栈为空或栈顶是左括号—>直接压入当前运算符
    • 如果栈顶是运算符,则进行运算符优先级比较。栈顶元素 \ge 待入栈元素,则栈顶元素弹出(连续判断),待入栈元素压栈。 ()(/%)(+)(\wedge),(*,/,\%),(+,-)
    • \wedge 具有右结合性,遇到连续的\wedge时不弹出栈顶的\wedge
  • 遇到函数调用:函数名(如pow)视为操作符压栈,遇到时开始参数收集

  • 遇到逗号:触发函数参数分隔(输出#标记)

  1. 表达式扫描完毕
  • 弹出栈中所有剩余运算符并加入输出列表
  1. 输出结果
  • 将输出列表连接成字符串即为后缀表达式

举例:求 pow(2 + 3 ^ 2 ^ 1, max(4 * 2 + 1, 3)) + 5 * 2 * 1 对应的后缀表达式

  • 处理指数运算 3 ^ 2 ^ 1(右结合性):先计算 2 ^ 1 → 后缀为 2 1 ^,再计算 3 ^ (结果) → 后缀为 3 2 1 ^ ^

  • 处理加法 2 + (3 ^ 2 ^ 1) :前缀 2 与步骤 1 的结果相加 → 后缀为 2 3 2 1 ^ ^ +

  • 处理 max 函数的参数 4 * 2 + 1 和 3

    • 计算 4 * 2 + 1:先 4 2 *,再加 1 → 后缀为 4 2 * 1 +
    • max 函数的两个参数为步骤 3 的结果和 3 → 后缀为 4 2 * 1 + 3 max
  • 处理 pow 函数:两个参数分别为步骤 2 的结果和步骤 3 的结果 → 后缀为 2 3 2 1 ^ ^ + 4 2 * 1 + 3 max pow

  • 处理乘法 5 * 2 * 1(左结合性):先 5 2 *,再乘以 1 → 后缀为 5 2 * 1 *

  • 处理最终加法:步骤 4 的结果与步骤 5 的结果相加 → 整体后缀为 2 3 2 1 ^ ^ + 4 2 * 1 + 3 max pow 5 2 * 1 * +

后缀表达式求值
  1. 初始化一个空栈
  • 用于存储操作数
  1. 从左到右扫描表达式
  • 遇到操作数:直接压入栈
  • 遇到运算符:从栈顶弹出所需数量的操作数(如+ 需要 2 个)计算运算结果,并将结果压回栈
  • 遇到函数调用:根据函数参数数量(如max需要 2 个)弹出相应数量的操作数,计算结果后压回栈
  1. 扫描结束
  • 栈中剩下的唯一元素就是最终结果
后缀转中缀
  1. 遇到操作数:压入栈

  2. 遇到运算符:弹出栈顶的 2 个操作数,组合成 (操作数1 运算符 操作数2),再压回栈

  3. 遇到函数调用:弹出相应数量的参数,组合成 函数名(参数1, 参数2),再压回栈

    • 每个函数调用有默认的参数个数,比如 max 默认就是 2 个参数,如果 max 是 3 个参数,需要在后缀表达式中显示声明为 3max
  4. 遇到#:标记参数的分隔,用于函数调用

3.3、栈在递归中的应用

若在一个函数、过程或数据结构的定义中又应用了它自身,则这个函数、过程或数据结构称为是递归定义的,简称递归

int F(int n){
    if(n==0)		
        return 0;	//边界条件
    else if(n==1)	
        return 1;	//边界条件
    else
        return F(n-1)+F(n-2);	//递归表达式
}

递归模型不能是循环定义的,必须满足以下两个条件

  • 递归表达式(递归体)
  • 边界条件(递归出口)

递归的精髓在于能否将原始问题转换为属性相同但规模较小的问题

3.4、队列在层次遍历中的应用

在信息处理时一类问题需要逐层或逐行处理。这类问题的解决方法就是在处理当前层或当前行时对下一层或下一行进行预处理(放入队列)等到当前层/行处理完毕,就可以处理下一层/行**(二叉树层次遍历)**

Snipaste_2025-11-10_09-22-58.png

3.5、队列在计算机系统中的应用

队列在计算机系统中的应用非常广泛,以下仅从两个方面阐述

  1. 解决主机与外部设备速度不匹配问题(设立数据缓冲区)
  2. 解决由多用户引起的资源竞争问题(设立资源请求队列)

4、数组和特殊矩阵

4.1、数组的定义

相同类型数据元素构成的有限序列,每个数据元素称为一个数组元素,每个元素在 nn 个线性关系中的序号称为该元素的下标

数组是线性表的推广,一维数组可视为一个线性表,二维数组可视为其元素是定长数组的线性表。数组一旦被定义除了结构的初始化和销毁外,数组只会有存取元素和修改元素的操作

4.2、数组的存储结构

大多数计算机语言都提供了数组数据结构,逻辑上的数组可采用计算机语言中的数组数据类型进行存储,一个数组的所有元素在内存中占用一段连续的存储空间。一维数组 A[0n1]A[0…n-1] ,其存储结构关系式为 LOC(ai)=LOC(a0)+i×L(0i<n) LOC(a_i)=LOC(a_0)+i×L(0\le i\lt n) ,其中 L 是每个数组所占的存储单元

对于多维数组,有两种映射方法:按行优先和按列优先(二维数组为例)

按行优先

先行后列,先存储行号较小的元素,行号相等先存储列号较小的元素

设二维数组的行下标和列下标的范围分别是[0,h1][0,h_1][0,h2][0,h_2],其存储结构关系式为:LOC(ai,j)=LOC(a0,0)+[i×(h2+1)+j]×LLOC(a_{i,j})=LOC(a_{0,0})+[i×(h_2+1)+j]×L

按列优先

当以列优先存储时,存储结构关系式为:LOC(ai,j)=LOC(a0,0)+[j×(h1+1)+i]×LLOC(a_{i,j})=LOC(a_{0,0})+[j×(h_1+1)+i]×L

4.3、特殊矩阵的压缩存储

压缩矩阵

指为多个值相同的元素只分配一个存储空间,对零元素不分配空间

特殊矩阵

指具有多个相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布有一定规律的矩阵

常见:对成矩阵、上三角矩阵、下三角矩阵、对角矩阵等

特殊矩阵的压缩方法

找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布的、值相同的多个矩阵元素压缩存储到一个存储空间中

对称矩阵

若对一个nn阶矩阵AA中的任意一个元素ai,ja_{i,j},都有ai,j=aj,ia_{i,j}=a_{j,i},则称其为对称矩阵

对于nn阶对称矩阵,上三角区和下三角区对应元素相同,因此只存储上三角或下三角(含主对角线)的元素,于是原本要存储n2n^2个元素变为存储n(n+1)2\frac{n(n+1)}{2}个元素

数组下标从 0 开始,矩阵行列从 1 开始,元素下标之间的对应关系:k={i(i1)2+j1ijj(j1)2+i1i<jk=\begin{cases} \frac{i(i-1)}{2}+j-1&i\ge j \\ \frac{j(j-1)}{2}+i-1&i\lt j \end{cases}

三角矩阵

上三角(下三角)的所有元素均为同一常量,其存储思想与对称矩阵类似,不同的是在存储完上三角(下三角)后,还要存储常量 1 次,所以可将nn阶下三角矩阵 AA 压缩存储在B[n(n+1)2+1]B[\frac{n(n+1)}{2}+1]

元素下标对应关系:k={i(i1)2+j1ijn(n1)2i<jk=\begin{cases} \frac{i(i-1)}{2}+j-1&i\ge j \\ \frac{n(n-1)}{2}&i\lt j \end{cases}

上三角按行存储:k={(i1)(2ni+2)2+(ji)ijn(n+1)2i<jk=\begin{cases} \frac{(i-1)(2n-i+2)}{2}+(j-i)&i\ge j \\ \frac{n(n+1)}{2}&i\lt j \end{cases}

三对角矩阵

也称为带状矩阵,所有非零元素都集中出现在以主对角线为中心的对角线的区域,其他元素均为 0

三对角矩阵,就是以主对角线为中心两边各一条,五对角线则,主对角线两边各两条

4.4、稀疏矩阵

定义

矩阵中非零元素的个数,相对于矩阵元素的个数来说非常的少,这样的矩阵称为稀疏矩阵

稀疏矩阵要保存的信息

将非零元素及其相应的行和列构成一个三元组(行标、列表、元素值),然后按照某种规律存储这些三元组线性表。但由此稀疏矩阵压缩存储后便失去了随机存取特性

稀疏矩阵的存储结构

稀疏矩阵的三元组既可以采用数组存储,又可以采用十字链表存储