图解数据结构:盘点链表与栈和队列的那些血缘(单双链表模拟实现栈和队列)

news2024/11/8 11:52:15

写在前面

 Hello,各位盆友们,我是黄小黄。关于前一段时间为什么拖更这件事,这里给大家说一句抱歉。笔者前段时间忙于ddl和一些比赛相关的事件,当然还有些隐藏任务,所以博文更新就放缓了。
在这里插入图片描述
 这里还需要做一下对以后博文布局的声明:笔者会尽量减少推广类的内容,着重文章质量的本身,并且由于数据结构非常需要读者自己独立思考,将思考的实现过程内化,并外现成对应的代码,是不能逾越的思考过程。为了避免文章冗余度过高,因此,在分步阐述的时候,这里只展现伪代码或者以文字和图片的形式阐述思路(对于数据结构来说,解法可能不唯一,也欢迎大家与小黄一起交流!),每部分内容结束附上参考代码。最后,感谢这半年来,大家对小黄的支持!
 话不多说,今天,我们重新回顾一下,之前所讲到的数据结构(栈、队列、单双链表),并尝试使用一种新的存储形式来组织数据(链式存储栈和队列) 算是对之前学习的一次巩固和升华?
在这里插入图片描述


文章目录

  • 写在前面
  • 1 栈和队列回顾
  • 2 双向链表实现栈
    • 2.1 思路点拨
    • 2.2 参考代码及测试
    • 2.3 单向链表实现栈
  • 3 双向链表实现队列
    • 3.1 思路点拨
    • 3.2 参考代码及测试
  • 4 单链表实现队列
    • 4.1 思路点拨
    • 4.2 参考代码及测试
  • 写在最后


1 栈和队列回顾

何为栈?何为队列?

用一句话来概括:所谓栈与队列,均是受限制的线性表。对于栈,其有着先进后出的特性,对于队列,其特性就是先进先出。
举个大白栗子:1、2、3、4、5依次入栈或者入队,对于栈,其出栈顺序就是5、4、3、2、1,对于队列,则是1、2、3、4、5。

不知道小伙伴有没有发现,正因为,栈和队列是被限制的线性表,所以,反而相较正常的线性表缺失了些功能?比如:任意位置插入,任意位置删除等等… …
咱也不装了,这不有手就行,更简单了嘛!

来,咱们看一下Java中的集合类,其体系图如下:

 LinkedList实现了List接口,但是在其之前还有个叫Deque的东西。而LinkedList也实现了Deque这一接口。
在这里插入图片描述
 所谓Deque,即double ended queue,是一种双端队列。该队列既可以从队头入队出队,也可以从队尾入队出队。也正是由于这一特性,Deque也常常用来代替Stack。

 而其实现子类LinkedList我们就很熟悉了,该类中封装了众多方法。其底层就是我们熟悉的双向链表的结构。同时,在其中,也使用了last指针维护链表的最后一个元素。正因为如此,LinkedList可以实现从头往后,从后往前的两种遍历方式。不仅如此,你也可以在 O(1) 的时间内,做到从头插入、从头删除、从尾插入、从尾删除(成对使用,这就很nice了,既可以当作队列,也可以当作栈,还可以当作普通的线性表使用!)。

 回顾了这么多,我们今天主要聊的就是如何使用单双链表实现栈和队列,这里有一个大前提,注意了!!!

由于我们之前使用顺序表去维护这两个数据结构时,其入栈、出栈;入队、出队的时间复杂度都是 O(1),因此,我们使用单双链表去模拟时,也必须满足时间复杂度为常数时间!

 学习,就是个由易到难,由入门到入土的… … …呃呃,到精通的过程。我们先尝试使用最简单的双向链表来实现栈和队列。

为什么说简单呢?因为LinkedList双向链表维护了一个last,始终指向最后一个节点,因此,无论是头插、头删,还是尾插、尾删,都可以做到时间复杂度为O(1)的情况下实现。

在这里插入图片描述


2 双向链表实现栈

2.1 思路点拨

 由于双向链表的特性,且用 last 维护了最后一个节点。对于栈的实现可以采用两种方式:

  1. 尾插法+尾删法
  2. 头插法+头删法

这里举例为:头插法和头删法的方式,其示意图如下:
在这里插入图片描述
无论是头插法还是头删法,其时间复杂度均为O(1)

2.2 参考代码及测试

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * 双向链表实现栈
 */
@SuppressWarnings({"all"})
public class LinkedStack {

    private Node head; // 头指针

    private Node last; // 始终指向链表的最后一个节点

    class Node{
        public int data;
        public Node pre; // 指向前一个节点
        public Node next; // 指向后面一个节点

        public Node(int data){
            this.data = data;
        }
    }

    public void push(int data){
        addFirst(data);
    }

    public int pop(){
        return removeFirst();
    }

    public int peek(){
        return head == null ? null : head.data; // 如果为head为空会报空指针异常 也可以自己处理 或者打印信息
    }

    //头插法
    public void addFirst(int data){
        Node newNode = new Node(data);
        // 如果head为空,即是第一个节点
        if (head == null){
            head = newNode;
            last = newNode;
            return;
        }
        // 正常的头插法
        newNode.next = head;
        head.pre = newNode;
        head = newNode;
    }

    // 头删法
    public int removeFirst(){
        // 如果为空
        if (head == null){
            throw new RuntimeException("当前为空,无法出栈");
        }
        // 如果仅剩下一个节点
        if (head.next == null){
            int val = head.data;
            head = null;
            last = null;
            return val;
        }
        // 头部删除
        int val = head.data;
        head = head.next;
        return val;
    }

    //得到栈的长度
    public int size(){
        int len = 0;
        Node cur = head;
        while (cur != null){
            len++;
            cur = cur.next;
        }
        return len;
    }


    // 清空栈
    public void clear(){
        Node cur = head;
        while (cur != null){
            Node curNext = cur.next;
            cur.pre = null;
            cur.next = null;
            cur = curNext;
        }
        head = null;
        last = null;
    }
}

在这里插入图片描述

2.3 单向链表实现栈

 参考双向链表实现栈采用头插法和头删法分别实现入栈和出栈操作,可以看出来,压根就没有使用到维护的last指针。 所以,对于单链表来说,头插法和头删法时间复杂度也是O(1),因此,很容易就能使用这种方式通过单链表的方式,实现栈。
 但是需要特别注意,采用尾插和尾删的方法是不行的!对于单链表来说,由于没有维护 last 指针,并且每次进行尾插的时候都需要遍历到 last 处。
对于尾删操作,每次都需要遍历到last的前一位置,才能实现删除操作(单链表无法实现自身删除)。
对于单链表来说,尾插和尾删的时间复杂度均为O(n)。
在这里插入图片描述


3 双向链表实现队列

3.1 思路点拨

 同理,对于使用双向链表实现队列,可以考虑以下两种方式:

  1. 采用头插法+尾删法;
  2. 采用头删法+尾插法;

这里我们以 头插法+尾删法 为例,实现双向链表模拟队列,示意图如下:
在这里插入图片描述
由于双向链表在删除时,不需要知道其邻前节点的位置,可以实现自身删除。所以,对于双向链表实现队列来说,无论是头插法还是尾删,时间复杂度均为O(1)

3.2 参考代码及测试

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * 使用双向链表模拟队列
 * 采用头插法 + 尾删法的方式
 */
@SuppressWarnings({"all"})
public class LinkedQueue {
    private Node head; // 头指针

    private Node last; // 始终指向链表的最后一个节点

    class Node{
        public int data;
        public Node pre; // 指向前一个节点
        public Node next; // 指向后面一个节点

        public Node(int data){
            this.data = data;
        }
    }

    // 入队
    public void offer(int data){
        addFirst(data);
    }

    // 出队
    public int poll(){
        return removeLast();
    }

    // 获取队头元素
    public int peek(){
        return last == null ? null : last.data; // 如果为head为空会报空指针异常 也可以自己处理 或者打印信息
    }

    //头插法
    public void addFirst(int data){
        Node newNode = new Node(data);
        // 如果head为空,即是第一个节点
        if (head == null){
            head = newNode;
            last = newNode;
            return;
        }
        // 正常的头插法
        newNode.next = head;
        head.pre = newNode;
        head = newNode;
    }

    // 头删法
    public int removeLast(){
        // 如果为空
        if (head == null){
            throw new RuntimeException("当前为空,无法出队");
        }
        // 如果仅剩下一个节点
        if (last == head){
            int val = last.data;
            head = null;
            last = null;
            return val;
        }
        // 尾部删除
        int val = last.data;
        last = last.pre;
        return val;
    }

    //得到队列的长度
    public int size(){
        int len = 0;
        Node cur = head;
        while (cur != null){
            len++;
            cur = cur.next;
        }
        return len;
    }


    // 清空队列
    public void clear(){
        Node cur = head;
        while (cur != null){
            Node curNext = cur.next;
            cur.pre = null;
            cur.next = null;
            cur = curNext;
        }
        head = null;
        last = null;
    }
}

在这里插入图片描述


4 单链表实现队列

4.1 思路点拨

思考一个问题

我们可以尝试双向链表类似的方式套用到单链表上,以实现队列吗?

照猫画虎,单链表我们也维护一个指针last,让其始终指向最后一个节点,我们来分析以下时间复杂度:

  • 对于 头插法和头删法, 由于头不存在紧邻前节点,因此,时间复杂度都为 O(1);
  • 对于尾插法,由于维护了一个 last 始终指向最后一个节点,所以,在尾插的时候,不需要再遍历,典型的空间换时间,因此时间复杂度为O(1);
  • 对于尾删法,由于是单向链表,所以在删除的时候,始终需要知道要删除节点的前一个节点,也就是,逃离不了遍历的命运。而,last随着元素个数的减少,还需要更新尾节点的位置,就更没办法更新了。显然,时间复杂度是O(n)。

显然,单链表具有局限性, 我们不能采用像双向链表一样,采用 头插 + 尾删 的方式模拟队列(限定了时间复杂度为O(1)),因此,我们考虑使用另一种方式,头删 + 尾插! 示意图如下:
在这里插入图片描述
对于头删和尾插,时间复杂度均为O(1)

4.2 参考代码及测试

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * 单链表实现队列
 * 尾插 + 头删
 */
@SuppressWarnings({"all"})
public class ListQueue {
    public Node head; // 指向链表的头
    public Node last; // 指向链表的尾

    class Node{
        public int val; // 存储的数据
        public Node next; // 存储下一个节点的地址

        public Node(int val) {
            this.val = val;
        }
    }

    // 入队列
    public void offer(int val){
        addLast(val);
    }

    // 出队列
    public int poll(){
        return removeFirst();
    }

    // 查看队头元素
    public int peek(){
        if (head == null){
            throw new RuntimeException("当前队列为空!");
        }
        return head.val;
    }

    // 头删法
    public int removeFirst(){
        // 判空
        if (head == null){
            throw new RuntimeException("当前队列为空!");
        }
        // 考虑只剩下一个节点
        if (head.next == null){
            int val = head.val;
            head = null;
            last = null;
            return val;
        }
        // 头部删除
        int val = head.val;
        head = head.next;
        return val;
    }

    // 尾插法
    public void addLast(int val){
        Node newNode = new Node(val);
        if (head == null){
            // 链表为空
            head = newNode;
            last = newNode;
            return;
        }
        // 尾插
        last.next = newNode;
        last = last.next;
    }

    // 返回队列的长度
    public int size(){
        int count = 0;
        Node cur = head;
        while (cur != null){
            count++;
            cur = cur.next;
        }
        return count;
    }

}

在这里插入图片描述


写在最后

本文被 Java数据结构 收录点击订阅专栏 , 持续更新中。
 创作不易,如果你有任何问题,欢迎私信,感谢您的支持!

在这里插入图片描述

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

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

相关文章

计算机原理一_计算机的组成、进程与线程

目录儿一、计算机组成二、进程与线程2.1 线程的切换2.2 CPU的并发控制2.2.1 关中断2.2.2 缓存一致性协议2.2.2.1 缓存Cache2.2.2.2 缓存行Cache Line2.2.2.3 缓存一致性拓展:超线程2.2.3 内存屏障2.2.3.1 CPU的乱序执行拓展1:java 的 this 溢出问题拓展2…

Linux(一):Linux基本结构

一、Linux系统划分 linux系统分为用户区、内核区 1.1分区目标 保护数据和硬件安全,对系统进行分区也就是进程分区,当处于用户态,只能访问用户区,用户无法修改内核,保证硬件安全,操作系统不易损坏&#…

【Qt】QMainWindow应用程序窗口类简单介绍

QMainWindow介绍 QMainWindow是一个为用户提供主窗口程序的类,是许多应用程序的基础,包含的组件有: 菜单栏QMenuBar,一个主窗口最多只能有一个菜单栏;包含一个下拉菜单项的列表,这些菜单项由QAction动作类…

【git版本控制】| git版本控制操作命令(全)

文章目录一、简介二、工作模式1 集中式(CVS、SVN)2 分布式Git三、Git1 工作模式2 git工作流程3 工作区和版本库4 注意事项5 基本操作5.1 创建本地版本库5.2 初始化本地版本库5.3 .git目录的作用5.4 创建用户5.5 其他操作6 git分支7 常见警告8 免密登录9 …

interface接口--GO面向对象编程思想

一、interface接口 interface 是GO语言的基础特性之一。可以理解为一种类型的规范或者约定。它跟java,C# 不太一样,不需要显示说明实现了某个接口,它没有继承或子类或“implements”关键字,只是通过约定的形式,隐式的…

【C语言进阶】自定义类型:结构体,枚举,联合体

目录 1、结构体的声明 1.1 结构体基础知识 1.2 结构体的声明 1.3 特殊的声明 1.4 结构体的自引用 1.5 结构体变量的定义和初始化 1.6 结构体内存对齐 ​编辑1.7 修改默认对齐数 1.8 结构体传参 2. 位段 2.1 什么是位段 2.2 位段的内存分配 2.3 位段的跨平台问…

【owt-server】代码结构及新增一个agent

owt server 官方 5.0 仓库:代码结构 manage console manage api portal sip portal 与agent 并列 agent又有很多种类。 启动脚本 启动一个新的agent 比如streaming-agent streaming-agent )cd ${OWT_HOME}/s

分布式id

分布式id一 什么是分布式系统唯一ID二 分布式系统唯一ID的特点三 分布式系统唯一ID的实现方案3.1 基于UUID3.2 基于数据库自增id3.3 基于数据库集群模式3.4 基于Redis模式3.5 基于雪花算法(Snowflake)模式3.6 百度(uid-generator)…

Python爬虫数据到sqlite实例

参考链接:https://blog.csdn.net/qq_45775027/article/details/115319253最近需要使用到爬虫数据库,原文中作者有些没补齐,略作修改之后跑通了。主要修改:1.调整了数据获取的正则表达式;2. 改了一下数据库的table名和定义名字&…

基于Java+SpringBoot+vue+element实现前后端分离牙科诊所管理系统详细设计

基于JavaSpringBootvueelement实现前后端分离牙科诊所管理系统详细设计 博主介绍:5年java开发经验,专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 欢迎点赞 收藏 ⭐留言 文末获取源码联系方式 文章目…

【Linux】虚拟地址空间 --- 虚拟地址、空间布局、内存描述符、写时拷贝、页表…

该吃吃,该喝喝,遇事儿别往心上隔😎 文章目录一、虚拟地址空间1.虚拟地址的引出(看不到物理地址,只能看看虚拟地址喽)2.虚拟地址空间布局(五个段)3.感性理解一下虚拟地址空间&#xf…

【C++修炼之路】C++入门(上)

👑作者主页:进击的安度因 🏠学习社区:进击的安度因(个人社区) 📖专栏链接:C修炼之路 文章目录一、前言二、第一个 C 程序三、C 关键字(C98)四、命名空间1、命名空间的定义2、命名空间…

C++ Prime课后习题第一章编程

编程一个C程序&#xff0c;它显示您的姓名和地址。#include <iostream>int stonetolb(int); int main() {using namespace std;cout << "zzz ";cout << "闵行"<<endl;return 0; }编写一个程序&#xff0c;要求用户输入一个以long…

3台机器配置hadoop集群_Hadoop+Hbase 分布式集群架构

安装搭建Hadoop1、 配置说明本次集群搭建共三台机器&#xff0c;具体说明下&#xff1a;主机名IP说明nn01192.168.1.51DataNode、NodeManager、ResourceManager、NameNodedn01192.168.1.52DataNode、NodeManager、SecondaryNameNodedn02192.168.1.53DataNode、NodeManager2 、安…

基于浏览器的 PDF 编辑器:RAD PDF for ASP.NET

版本 3.34 改进的 PDF 收藏/投资组合支持和服务器 API 改进 Ω578867473功能更新 为更基本的 PDF 文件损坏/语法错误添加了更正添加了 PdfButtonField.NamedAction 属性添加了 PdfButtonField.IsNamedAction 属性添加 PdfButtonField() 构造函数 - PdfButtonFields 可以由服…

unity-常用组件实操案例

文章目录transform摄像机cameraskybox相机权重&#xff08;depth&#xff09;Audio sourcevideo playertransform 不但控制着组件的旋转、位置、缩放并且还控制着组件间的父子关系 using System; using System.Collections; using System.Collections.Generic; using UnityEn…

不锈钢企业如何利用APS排程软件提升管理效益?

保温杯一般是由陶瓷或不锈钢加上真空层做成的盛水容器&#xff0c;顶部有盖&#xff0c;密封严实&#xff0c;真空绝热层能使装在内部的水等液体延缓散热&#xff0c;以达到保温的目的。保温杯从保温瓶发展而来的&#xff0c;保温原理与保温瓶一样&#xff0c;只是人们为了方便…

Collection

面向对象语言对事物的体现都是以对象的形式&#xff0c;所以为了方便对多个对象的操作&#xff0c;就对对象进行存储&#xff0c;集合就是存储对象最常用的一种方式 数组和集合的不同&#xff1a; 数组长度是固定的&#xff1b;集合长度是可变的。 数组中可以存储基本数据类…

C#在控制台中打印进度条【同步和异步】

使用控制台打印进度条的简单方法。 有现成的IProgress接口进行操作&#xff1a; 实例&#xff1a; var prog new Progress<double>((theV > {Console.WriteLine($"Now the Progress&#xff1a;" COUNT / 10.0 * 100 "%" new string(#, COUN…

社科院与杜兰大学金融管理硕士---授人以鱼不如授人以渔,培养全新金融人才

古人云&#xff1a;“授人以鱼&#xff0c;三餐之需&#xff1b;授人以渔&#xff0c;终身之用”。都说职场入战场&#xff0c;一入职场就如履薄冰。走的每一步可能都影响着自己的职业生涯。在职场无烟的战争中&#xff0c;会慢慢发现差距一点点的被拉开了。金融是现代经济的血…