《数据结构》:03栈和队列

本文详细介绍了栈和队列这两种数据结构的基本概念,包括它们的操作、存储结构(顺序和链式)以及特殊实现如共享栈和循环队列。栈是后进先出(LIFO)结构,常用于括号匹配和表达式求值等问题;队列是先进先出(FIFO)结构,常见于层次遍历和资源调度。此外,还讨论了它们在计算机系统中的应用,如缓冲区管理和多任务处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

栈的基本概念

栈(Stack)是只允许在一端进行插入或删除操作的线性表。首先栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作,如图所示:

在这里插入图片描述

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

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

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

栈的基本操作

方法描述
InitStack(&S)初始化一个空栈S
StackEmpty(S)判断一个栈是否为空,若栈S为空则返回true,否则返回false
Push(&sx)进栈,若栈S未满,则将x加入使之成为新栈顶
Pop(&s,&x)出栈,若栈S非空,则弹出栈顶元素,并用x返回
GetTop(S,&x)读栈顶元素,若栈S非空,则用x返回栈顶元素
Destroystack(&S)销毁栈,并释放栈s占用的存储空间(“&”表示引用调用)

栈的顺序存储结构

栈是一种操作受限的线性表,类似于线性表,它也有对应的两种存储方式。

顺序栈的实现

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

在这里插入图片描述

栈顶指针:S.top,初始时设置 S.top=-1栈顶元素:S.data[S.top]

**进栈操作:**栈不满时,栈顶指针先加 1,再送值到栈顶元素。

**出栈操作:**栈非空时,先取栈顶元素值,再将栈顶指针减 1。

栈空条件:S.top==-1栈满条件:S.top==MaxSize-1栈长:S.top+1

由于顺序栈的入栈操作受数组上界的约束,当对栈的最大使用空间估计不足时,有可能发生栈上溢,此时应及时向用户报告消息,以便及时处理,避免出错。

共享栈的实现

利用栈底位置相对不变的特性,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。如图所示:

在这里插入图片描述

两个栈的栈顶指针都指向栈顶元素,top0=-1 时 0 号栈为空,top1=MaxSize 时 1号栈为空;仅当两个栈顶指针相邻(top1-top0=1)时,判断为栈满。当 0 号栈进站时 top0 先加 1 再赋值,1号栈进栈时 top1 先减 1 再赋值;出栈时则刚好相反。

共享栈是为了更有效地利用存储空间两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢。其存取数据的时间复杂度均为 O(1),所以对存取效率没有什么影响。

栈的链式存储结构

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

在这里插入图片描述

采用链式存储,便于结点的插入与删除。链栈的操作与链表类似,入栈和出栈的操作都在链表的表头进行。需要注意的是,对于带头结点和不带头结点的链栈,具体的实现会有所不同。

队列的基本概念

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

在这里插入图片描述

**队头(Front):**允许删除的一端,又称队首。

**队尾(Rear):**允许插入的一端。

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

队列的基本操作

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

需要注意的是,栈和队列是操作受限的线性表,因此不是任何对线性表的操作都可以作为栈和队列的操作。比如,不可以随便读取栈或队列中间的某个数据。

队列的顺序存储结构

顺序队列的实现

队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针:队头指针 front 指向队头元素,队尾指针 rear 指向队尾元素的下一个位置。

在这里插入图片描述

初始状态(队空条件):Q.front==Q.rear==0

**进队操作:**队不满时,先送值到队尾元素,再将队尾指针加 1。

**出队操作:**队不空时,先取队头元素值,再将队头指针加 1。

循环队列的实现

将顺序队列臆造出一个环状的空间,即把存储队列元素的表从逻辑上视为一个环,称为循环队列。如图所示:

在这里插入图片描述

初始时: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

为了区分队空还是队满的情况,有三种处理方式:

  • 牺牲一个单元来区分队空和队满,入队时少用一个队列单元,这是一种较为普遍的做法,约定以 “队头指针在队尾指针的下一位置作为队满的标志”,如图(d2)所示。
    队满条件:(Q.rear+1)%MaxSize==Q.front
    队空条件仍:Q.front==Q.rearo
    队列中元素的个数:(Q.rear-Q.front+MaxSize)%MaxSize
  • 类型中增设表示元素个数的数据成员。这样,队空的条件为 Q.size==0;队满的条件为 Qsize==MaxSize。这两种情况都有Q.front==Q.rear
  • 类型中增设 tag 数据成员,以区分是队满还是队空。tag 等于 0 时,若因删除导致 Q.front==Q.rear,则为队空;tag 等于 1 时,若因插入导致 Q.front==Q.rear,则为队满。

队列的链式存储结构

链式队列的实现

队列的链式表示称为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表。头指针指向队头结点,尾指针指向队尾结点,即单链表的最后一个结点(注意与顺序存储的不同)。如图所示:

在这里插入图片描述

Q.front==NULLQ.rear==NULL 时,链式队列为空。

出队时,首先判断队是否为空,若不空,则取出队头元素,将其从链表中摘除,并让 Q.front 指向下一个结点(若该结点为最后一个结点,则置 Q.front 和 Q.rear 都为 NULL)。

入队时,建立一个新结点,将新结点插入到链表的尾部,并改让 Q.rear 指向这个新插入的结点(若原队列为空队,则令 Q.front 也指向该结点)。

双端队列的实现

双端队列是指允许两端都可以进行入队和出队操作的队列,其元素的逻辑结构仍是线性结构。将队列的两端分别称为前端和后端,两端都可以入队和出队。如图所示:

在这里插入图片描述

在双端队列进队时,前端进的元素排列在队列中后端进的元素的前面,后端进的元素排列在队列中前端进的元素的后面。在双端队列出队时,无论是前端还是后端出队,先出的元素排列在后出的元素的前面。

**输出受限的双端队列:**允许在一端进行插入和册除,但在另一端只允许插入的双端队列称为输出受限的双端队列。如图所示:

在这里插入图片描述

**输入受限的双端队列:**允许在一端进行插入和删除,但在另一端只允许删除的双端队列称为输入受限的双端队列,若限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就蜕变为两个栈底相邻接的栈。如图所示:

在这里插入图片描述

栈和队列的应用

栈在括号匹配中的应用

假设表达式中允许包含两种括号:圆括号和方括号,其嵌套的顺序任意即 ([]())[([][])] 等均为正确的格式,[(])([()])(()] 均为不正确的格式。

考虑下列括号序列:

[ ( [ ] [ ] ) ]

算法的思想如下:

  1. 初始设置一个空栈,顺序读入括号。
  2. 若是右括号,则或者使置于栈顶的最急迫期待得以消解,或者是不合法的情况(括号序列不匹配,退出程序)。
  3. 若是左括号,则作为一个新的更急迫的期待压入栈中,自然使原有的在栈中的所有未消解的期待的急迫性降了一级。算法结束时,栈为空,否则括号序列不匹配。

栈在表达式求值中的应用

表达式求值是程序设计语言编译中一个最基本的问题,它的实现是栈应用的一个典型范例。中缀表达式不仅依赖运算符的优先级,而且还要处理括号。后缀表达式的运算符在操作数后面,在后缀表达式中已考虑了运算符的优先级,没有括号,只有操作数和运算符。中缀表达式 A+B*(C-D)-E/F 所对应的后缀表达式为 ABCD-*+EF/-

后缀表达式 ABCD-*+EF/- 求值的过程

在这里插入图片描述

栈在递归中的应用

递归是一种重要的程序设计方法。简单地说,若在一个函数、过程或数据结构的定义中又应用了它自身,则这个函数、过程或数据结构称为是递归定义的,简称递归。

它通常把一个大型的复杂问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的代码就可以描述出解题过程所需要的多次重复计算,大大减少了程序的代码量。但在通常情况下,它的效率并不是太高。

递归的一个典型例子:

int Fib(int n) {
    if(n==0)
        return 0;
    else if(n==1)
        return 1;
    else
        return Fib(n-1)+Fib(n2);
}

必须注意递归模型不能是循环定义的,其必须满足下面的两个条件:

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

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

在递归调用的过程中,系统为每一层的返回点、局部变量、传入实参等开辟了递归工作栈来进行数据存储,递归次数过多容易造成栈溢出等。而其效率不高的原因是递归调用过程中包含很多重复的计算。下面以 n=5 为例,列出递归调用执行过程。如图所示:

在这里插入图片描述

显然,在递归调用的过程中,Fib(3) 被计算了2次,Fib(2) 被计算了3次。Fib(1) 被调用了5次,Fib(0) 被调用了3次。所以,递归的效率低下,但优点是代码简单,容易理解。

队列在层次遍历中的应用

在信息处理中有一大类问题需要逐层或逐行处理。这类问题的解决方法往往是在处理当前层或当前行时就对下一层或下一行做预处理,把处理顺序安排好,待当前层或当前行处理完毕,就可以处理下一层或下一行。使用队列是为了保存下一步的处理顺序。用二叉树层次遍历的例子,说明队列的应用。如图所示:

在这里插入图片描述

层次遍历二叉树的过程:

在这里插入图片描述

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

队列在计算机系统中的应用非常广泛,以下仅从两个方面来简述队列在计算机系统中的作用:第一个方面是解决主机与外部设备之间速度不匹配的问题,第二个方面是解决由多用户引起的资源竞争问题

对于第一个方面,仅以主机和打印机之间速度不匹配的问题为例做简要说明。主机输出数据给打印机打印,输出数据的速度比打印数据的速度要快得多,由于速度不匹配,若直接把输出的数据送给打印机打印显然是不行的。解决的方法是设置一个打印数据缓冲区,主机把要打印输出的数据依次写入这个缓冲区,写满后就暂停输出,转去做其他的事情。打印机就从缓冲区中按照先进先出的原则依次取出数据并打印,打印完后再向主机发出请求。主机接到请求后再向缓冲区写入打印数据。这样做既保证了打印数据的正确,又使主机提高了效率。由此可见,打印数据缓冲区中所存储的数据就是一个队列。

对于第二个方面,CPU(即中央处理器,它包括运算器和控制器)资源的竞争就是一个典型的例子。在一个带有多终端的计算机系统上,有多个用户需要 CPU 各自运行自己的程序,它们分别通过各自的终端向操作系统提出占用 CPU 的请求。操作系统通常按照每个请求在时间上的先后顺序,把它们排成一个队列,每次把 CPU 分配给队首请求的用户使用。当相应的程序运行结束或用完规定的时间间隔后,令其出队,再把 CPU 分配给新的队首请求的用户使用。这样既能满足每个用户的请求,又使 CPU 能够正常运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值