栈和队列讲解

news2025/5/26 2:51:51

栈和队列

  • 栈和队列
    • 3.1 栈和队列的定义和特点
    • 3.2 案例引用
    • 3.3 栈的顺序表示和实现
    • 3.4 栈的链式表示和实现
    • 3.5 队列的顺序表示和实现
    • 3.6 队列的链式表示和实现

3.1 栈和队列的定义和特点

栈 (stack) 是限定仅在表尾进行插入或删除操作的线性表。 因此, 对栈来说, 表尾端有其 特殊含义, 称为栈顶 (top), 相应地, 表头端称为栈底 (bottom/base)。 不含元素的空表称为空栈。

栈是按后进先出的原则进行的, 如 图(a) 所示。 因此, 栈又称为后进先出 (Last In First Out, LIFO) 的线性表

插入元素叫入栈(PUSH),删除元素叫弹栈(POP)

image-20230801212051890

栈与一般线性表的区别

image-20230801213722984

和栈相反,队列(queue)是一种**先进先出(First In First Out, FIFO)**的线性表。它只允许在表 的一端进行插入,而在另一端删除元素。

在队列中,允许插入的一端称为队尾(rear), 允许 删除的一端则称为队头(front)。

image-20230801212349071

3.2 案例引用

**【案例3.1】**进制转换

将十进制整数 N 向其他进制数d(二、八、十六)的转换

转换法则: N除以d倒取余

例如:将十进制数159转换成八进制数

image-20230801214650193

利用栈的先进后出,将每次计算结果入栈,最终取出来的就是结果

image-20230801214724287

**【案例3.2】**括号匹配的校验

假设表达式中允许包含两种括号: 圆括号和方括号

image-20230801214922504

遇见左括号就入栈,遇见右括号就与栈顶元素相比较,符合就弹栈,不符合就是不匹配。

image-20230801215514238

**【案例3.3】**表达式求值

这里介绍的算法是由运算符优先级确定运算顺序的对表达式求值算法—— 算符优先算法

表达式组成

操作数(Qperand): 常数、变量

运算符(operator): 算术运算符、关系运算符和逻辑运算符

界限符(delimiter) :左右括弧和表达式结束符。

任何一个算术表达式都由操作数(常数、变量)、算术运算符(+、-、/)和界限符 (括号、表达式结束符“#'、虚设的表达式起始符# )组成。后两者统称为算符。

例如:# 3* (7-1)#

image-20230801220624360

3.3 栈的顺序表示和实现

栈的抽象数据类型的定义

image-20230801220951700

顺序栈的表示和实现

由于栈本身就是线性表,于是栈也有顺序存储和链式存储俩种实现方式。顺序存储——顺序栈 | 链式存储——链栈

存储方式: 同一般线性表的顺序存储结构完全相同

利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素。栈底一般在低地址端。

使用俩个指针base、top:base指向栈底元素,top指向栈顶元素。但是为了操作方便,一般 top 指向栈顶的下一个存储地址。另外使用一个 stacksize 表示栈的最大容量。

image-20230801222241444

空栈时,base 和 top 同时指向栈底,没有元素可以弹栈,只能入栈

image-20230801222614910

满栈时,top 与 base 的差值等于stacksize,不允许在入栈,只能弹栈

image-20230801222755002

使用数组作为顺序栈存储方式特点

简单、方便,但是容易产生溢出【数组大小固定】

上溢(overflow):栈已经满,又要压入元素

下溢(underflow):栈已经空,还要弹出元素


代码实现栈的初始化

public class Stack {

    // 栈的最大容量
    public int stackSize;
    // top指针,指向栈顶
    public int top;
    // base指针,指向栈底
    public int base;
    // 使用数组模拟栈
    public Object[] stack;

    // 初始化栈
    public Stack(int stackSize) {
        this.stackSize = stackSize;
        stack = new Object[stackSize];
        top = 0;
        base = 0;
    }
    
    /**
     * 是否空栈
     * */
    public boolean isEmpty(){
        return top == base;
    }
    /**
     * 满栈
     * */
    public boolean isFull(){
        return  (top - base) == stackSize;
    }
    /**
     * 栈的长度
     * */
    public int getLength(){
        return  top - base;
    }
    /**
     * 清空栈
     * */
    public void clear(){
        this.top = this.base;
    }
    
}

顺序栈的入栈

  • 判断顺序栈是否已经满了,若满了抛出异常
  • 将元素压入栈
  • top指针+1
    /**
     * 入栈
     * */
    public void push(Object e) {
        if (isFull()) {
            throw new RuntimeException("栈满");
        }
        stack[top++] = e;
    }

顺序栈的出栈

  • 判断是否栈空,若空抛出异常
  • top指针-1
  • 弹出元素
    /**
     * 出栈
     * */
    public Object pop(){
        if (isEmpty()) {
            throw new RuntimeException("栈空");
        }
        return stack[--top];
    }

3.4 栈的链式表示和实现

链栈是运算受限的单链表,只能在链表头部进行操作

image-20230806104625248

通过上面的图片,可以看出链表的方向是和单链表相反的,从 an ~ a1,这样的目的主要是为了操作方便,栈的弹栈、入栈都是从表头开始的,这样不会影响后续结点

链栈的初始化:

public class ChainedStack {

    // 头指针
    Node s;

    public ChainedStack() {
        this.s = null;
    }
}

入栈操作:

image-20230806110521898

入栈结点p,将p结点的next域指向s,s重新指向栈顶

image-20230806110551495

    /**
     * 入栈
     * */
    public void push(Node p) {
        // 将新结点指向栈顶元素
        p.next = s;
        // 更改指针
        s = p
    }

弹栈操作:

将弹出的结点使用一个指针p保存,将s指向p.next

image-20230806110824188


    /**
     * 弹栈
     * */
    public Object pop(){
        if (s == null) {
            throw new RuntimeException("空栈");
        }
        Node res = s;
        s = res.next;
        return res.data;
    }

取栈顶元素

    /**
     * 取栈顶元素
     * */
    public Object getTop(){
        if (s == null) {
            throw new RuntimeException("空栈");
        }
        return s.data;
    }

3.5 队列的顺序表示和实现

image-20230802220932178

与栈不同的是,无论是弹栈、压栈始终是移动一个栈顶指针,而队列中,出队移动front头指针,入队移动rear尾指针。

image-20230802222012253

代码实现

public class Queue {
    int maxSize; // 队列的最大容量
    Object[] queue;
    int front; // 头指针
    int rear; // 尾指针

    public Queue(int maxSize) {
        this.maxSize = maxSize;
        queue = new Object[maxSize];
        front = 0 ;
        rear = 0;
    }

    // 是否为空队列
    public boolean isEmpty(){
        return front == rear;
    }

    // 是否为满队列
    public boolean isFull(){
        return rear == maxSize-1;
    }

    // 获取队列元素的个数
    public int getLength(){
        return front - rear;
    }

    // 入队
    public void add(Object e){
        if (isFull()) {
            throw new RuntimeException("满队");
        }
        queue[rear++] = e;
    }
    // 出队
    public Object out(){
        if (isEmpty()){
            throw new RuntimeException("空队");
        }
        Object o = queue[front];
        // 将出队的元素存放位置置空
        queue[front++] = null;
        return o;
    }

}

问题

image-20230802223515182

假设当 rear 已经等于 maxSize【黄色部分为队列长度】之后,还能入队吗?

答案是不能的,数组下标是从0开始的,rear= maxSize时,在入队就已经越界了。

其实我们发现下标从 0~front 之间是没有存储元素的,长度虽然为 6,但实际上存储了俩个元素,并没有真正的存满,这种情况就称为 假溢出

相对应的有真溢出,就是front = 0,rear = maxsize, 之间已经存满了元素,此时在入队就已经没有位置了。这种情况就无需处理了,而假溢出才是我们真正要解决的。

image-20230802224505194

解决 假溢出 情况:

使用循环队列的方式解决假溢出情况, 当 rear = maxSize 时,让 rear 重新从 指向 0 的位置,当入队时,插入在 0 的位置上

image-20230802225315575

那么怎么让 rear 重新变为0 呢? 可以利用模运算(取余)

当 rear =1,maxSize=6时,1% 6 = 1, 此时rear指向 1

当 rear =2,maxSize=6时,2% 6 = 2, 此时rear指向 2

当 rear =6,maxSize=6时,6% 6 = 0, 此时rear指向 0

插入元素e: queue[rear] = e; (rear+1) % maxSize
删除元素: e =  queue[front]; (front+1) % maxSize

循环队列如何判断空队列和满队列呢?

在循环队列中,可以发现无论是空队还是满队,都是 front == rear

image-20230802230157146

解决方案

1.另外设一个标志以区别队空

2.另设一个变量录元素个数

3.少用一个元素空间

我用的是第二种方式,易于理解。

代码实现

public class CircularQueues {

    Object[] queue;
    int front;
    int rear;
    int maxSize;
    int count; // 用来记录元素的个数

    // 初始化队列
    public CircularQueues(int maxSize) {
        this.maxSize = maxSize;
        this.front = 0;
        this.rear = 0;
        this.queue = new Object[maxSize];
        this.count = 0;
    }

    /**
     * 判断是否为空
     * */
    public boolean isEmpty() {
        return count == 0;
    }

    /**
     * 判断是否为满
     * */
    public boolean isFull() {
        return count == maxSize;
    }

    /**
     * 入队
     * */
    public void add(Object e) {
        if (isFull()) {
            throw new RuntimeException("满队");
        }
        // 入队操作
        queue[rear] = e;
        rear = (rear + 1) % maxSize;
        // 记录数+1
        count++;
    }

    /**
     * 出队
     * */
    public Object out() {
        if (isEmpty()) {
            throw new RuntimeException("空队");
        }
        Object e = queue[front];
        front = (front + 1) % maxSize;
        // 记录数-1
        count--;
        return e;
    }
    
        /**
     * 获取对头元素
     * */
    public Object getHeadEle() {
        if (isEmpty()) {
            throw new RuntimeException("空队");
        }
        return queue[front];
    }
}

3.6 队列的链式表示和实现

顺序队列采用数组实现,大小固定,若无法估计队列的长度,直接使用链式队列。

头指针的指向头结点,尾指针指向尾结点。

image-20230806095447728

链队列指针变化情况

初始情况下,头指针、尾指针都指向头结点

image-20230806100310551

链队列的初始化

public class ChainedQueues {
    // 头指针
    Node front;
    // 尾指针
    Node rear;
    // 头指针
    Node dummyHead;

    public ChainedQueues() {
        // 初始化,头结点的数据随意,可以不设置
        this.dummyHead = new Node(-1);
        this.front = dummyHead;
        this.rear = dummyHead;
    }
}

入队操作

头删尾插,将rear指针的next域指向新结点,并将 rear指针后移即可。

    /**
     * 入队
     * */
    public void add(Node e) {
        rear.next = e;
        rear = e;

    }

出队操作:

出队操作时将首元结点删除掉并返回,很简单,头指针的next域就是要出队的元素。

    public Object out() {
        if (rear == front) {
            throw new RuntimeException("队空");
        }
        Node res = front.next;
        front.next = res.next;
        return res.data;
    }

但是这样写会有一些问题,如果出队的正好是队尾元素,此时 front.next = null, 但是 rear 指针仍然在指向 res,因此判断非空时,会返回 false

因此当出队的是队尾元素还需要将 rear 指针重新指向头结点

image-20230806103305729

    /**
     * 出队
     * 如果出队的是最后一个结点,还要修改尾指针
     * */
    public Object out() {
        if (isEmpty()) {
            throw new RuntimeException("队空");
        }
        Node res = front.next;
        front.next = res.next;
        if (res == rear) {
            // 最后一个结点
            rear = dummyHead;
        }
        return res.data;
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1010447.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

树莓派4B使用Docker部署SpringBoot项目——(一)树莓派安装docker

使用Shell7连接树莓派,命令行执行安装命令,等待安装成功即可。 apt install docker.io执行docker version查看docker版本信息。 其他docker命令。 docker images 查看镜像 docker ps -a 查看所有容器 docker ps 查看运行的容器

社区分享|MeterSphere变身“啄木鸟”,助力云帐房落地接口自动化测试

云帐房网络科技有限公司(以下简称为“云帐房”)成立于2015年3月,以“成为最值得信赖的税务智能公司”为愿景,运用人工智能、大数据等互联网技术,结合深厚的财税行业服务经验,为代账公司和中大型企业提供智能…

Linux中使用Docker安装ElasticSearch7.10.x集群

使用Docker安装ElasticSearch7.10.x单节点请访问这里 一、集群环境说明 服务器IP地址192.168.137.1,192.168.137.2,192.168.137.3 二、前期准备 1. 拉取镜像 docker pull elasticsearch:7.10.12. 首先需要创建一个用于生成秘钥的初始容器&#xff0…

一体化研发协作赋能平台:Apipost

作为一款专为程序员打造的API管理工具,Apipost也成为开发人员圈子里的一款热门工具。Apipost拥有强大的功能和便捷操作性,这也让许多开发者爱不释手。那么,Apipost到底有哪些吸引人的特点呢?本文将为您详细介绍。 统一API管理 Ap…

qt 移植到vs后,常见问题汇总????

1.第一次在VS中编译QT项目,因为在MinGW中不能编译带有qtwebengine的程序,因为这个引擎使用的google浏览器的内核,据QT官方的说法:google不喜欢MinGW,所以QT5.5以后的版本中带有这个模块的的部分将无法编译通过,我们只能…

《银河麒麟高级服务器操作系统V10》使用

一言而论:讲了麒麟服务器V10的基本使用,包括终端、VNC 文章目录 前言基本架构环境硬件环境软件环境 麒麟安装步骤1.在宿主机上安装好VM,并且激活2.使用VM创建虚拟机3.启动虚拟机 终端常用点VNC的使用麒麟上安装VNC服务器Windows上安装VNC客户…

在ie浏览器下解决pdfjs插件思源宋体字体部分无法识别问题

pdf文件正常 利用pdfis渲染出来就成这样了 查看了思源宋体是2017年发布,pdf版本是1.10.88 ,推测可能由于版本问题部分字体映射没有,去官网拷贝了几个版本,在本地启服务测试了几个,为了兼顾ie浏览器兼容 ,选择了2.0.94…

JVM参数配置

一、堆内存相关配置 复制代码 设置堆初始值 指令1:-Xms2g 指令2:-XX:InitialHeapSize2048m ​ ​ 设置堆区最大值 指令1:-Xmx2g 指令2: -XX:MaxHeapSize2048m ​ ​ 缩小堆内存的时机 -XX:MaxHeapFreeRatio70//堆内存…

视频怎么抠图换背景,怎么把视频后面的背景换掉?

视频中的背景可以直接影响整个视频的观感,有时候我们需要更换背景来达到更好的效果。而如何更换背景呢? 在视频制作中,更换视频背景可以为视频添加更好的视觉效果,增强观赏性和吸引力。例如,在拍摄一个演讲视频时&…

C++之打印编译全过程(二百一十四)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生…

驱动开发--汇总

一,【驱动相关概念】 1,什么是驱动 能够驱使硬件实现特定功能的软件代码 根据驱动程序是否依赖于系统内核将驱动分为裸机驱动和系统驱动 2,逻辑驱动和系统驱动的区别 裸机驱动:编写的驱动代码中没有进行任何内核相关API的调用…

小程序实现一个 倒计时组件

小程序实现一个 倒计时组件 需求背景 要做一个倒计时,可能是天级别,也可能是日级别,时级别,而且每个有效订单都要用,就做成组件了 效果图 需求分析 需要一个未来的时间戳,或者在服务度直接下发一个未来…

LeetCode-热题100-笔记-day23

104. 二叉树的最大深度https://leetcode.cn/problems/maximum-depth-of-binary-tree/ 给定一个二叉树 root ,返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1: 输入:root [3,9,20,null,null,1…

leetcode 332. Reconstruct Itinerary(重构行程)

有一些票tickets, tickets[ i ] [from, to], 每个出发到达城市名字都是3个大写英文字母, 同一个出发城市时,优先去字母顺序较小的到达城市。 必须先从“JFK”出发。 每个ticket必须用且只用一次,所有ticket一定会形成至少一个有效的行程&…

重建大师提交空三后引擎状态是等待,怎么开启?

答:图片中这是在自由网空三阶段,整个AT都是等待中,可以修改任务目录和监控目录看一下,先设置引擎,再提交空三。

【Unity】万人同屏, 从入门到放弃之——多线程RVO避障

不使用Dots能否实现海量物体同屏?很多场面宏大的游戏,尤其是Rougelike游戏,动辄成千上万满屏怪,割草清屏的快感酣畅淋漓,所以这类游戏非常火爆,然鹅是怎么做到的呢? 首先,海量移动物…

CRC(循环冗余校验码的校验方法)

5个关键点: 1.信息码:即给出要校验的二进制码 2.生成多项式:一般多项式会给,从最高位的指数位数就可以得到有几个校验码;如果没给多项式,肯定会给个多项式二进制码,根据它来推就行(…

面试题:问js的forEach和map的区别

前端面试题库 (面试必备) 推荐:★★★★★ 地址:前端面试题库 【国庆头像】- 国庆爱国 程序员头像!总有一款适合你! 前言 为什么要写这么一篇文章,原因是今天下午水群的时候&…

【第四阶段】kotlin语言中的数组类型

1.kotlin语言中的数组类型 类型定义IntArrayintArrayOf()DoubleArraydoubleArrayOf()LongArraylongArrayOf()ShortArrayshortArrayOf()ByteArraybyteArrayOf()FloatArrayfloatArrayOf()BooleanArraybooleanArrayOf()Array<对象类型>arrayOf() 2.Intarry常规操作的越界崩…

MySQL数据库 | 手把手教你如何去下载安装MySQL数据库

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。MySQL是一种开源的关系型数据库管理系统&#xff0c;它可以用于存储和管理大量结构化数据。它提供了广泛的功能和灵活性&#xff0c;使得它成为许多应用程序的首选数据库解决方案。为了让大家后期更好的了解MySQL数据库基础…