数据结构与算法—双链表

前言

前面有很详细的讲过线性表(顺序表和链表),当时讲的链表以单链表为主,但在实际应用中双链表有很多应用场景,例如大家熟知的LinkedList。

image-20231031232421766

双链表与单链表区别

单链表和双链表都是线性表的链式实现,它们的主要区别在于节点结构。单链表的节点包含数据字段 data 和一个指向下一个节点的指针 next,而双链表的节点除了 datanext,还包含指向前一个节点的指针 pre。这个区别会导致它们在操作上有些差异。

单链表:

单链表的一个节点,有储存数据的data,还有后驱节点next(指针)。单链表想要遍历的操作都得从前节点—>后节点

image-20231031233306475

双链表:

双链表的一个节点,有存储数据的data,也有后驱节点next(指针),这和单链表是一样的,但它还有一个前驱节点pre(指针)。

image-20231031233635681

双链表结构的设计

上一篇讲单链表的时候,当时设计一个带头结点的链表就错过了不带头结点操作方式,这里双链表就不带头结点设计实现。所以本文构造的这个双链表是:不带头节点、带尾指针(tail)的双向链表。

对于链表主体:

public class DoubleLinkedList<T> {
    private Node<T> head;
    private Node<T> tail;
    private int size;
    public DoubleLinkedList(){
        this.head = null;
        this.tail = null;
        size = 0;
    }
    public void addHead(T data){}
    public void add(T data, int index){}
    public void addTail(T data){}
    public void deleteHead(){}
    public void delete(int index){}
    public void deleteTail(int index){}
    public T get(int index){}
    public int getSize() {
        return size;
    }
    private static class Node<T> {
        T data;
        Node<T> pre;
        Node<T> next;
        public Node() {
        }
        public Node(T data) {
            this.data = data;
        }
    }
}

具体操作分析

对于一个链表主要的操作还是增删,查询的话不做详细解释。

剖析增删其实可以发现大概有头插入、编号插入、末尾插入、头删除、编号删除、尾删除几种情况。然而这几种关于头尾操作的可能会遇到临界点比如链表为空时插入删除、或者删除节点链表为空。

这个操作是不带头结点的操作,所以复杂性会高一些!

头插入

头插入区分头为空和头不为空两种情况

头为空:这种情况head和tail都指向新节点

头不为空:

  1. 新节点的next指向head
  2. head的pre指向新节点
  3. head指向新节点(认新节点为head)

image-20231101232855888

尾插入

尾插需要考虑tail为null和不为null的情况。流程和头插类似,需要考虑tail指针最后的指向。

tail为null:此时head也为null,head和tail指向新节点。

tail不为null:

  • 新节点的pre指向tail
  • tail的next指向新节点
  • tail指向新节点
编号插入

按编号插入分情况讨论,如果是头插或者尾插就直接调用对应的方法。普通方法的实现方式比较灵活,可以找到前驱节点和后驱节点,然后进行指针插入,但是往往很多时候只用一个节点完成表示和相关操作,就非常考验对表示的理解,这里假设只找到preNode节点。
index为0:调用头插

index为size:调用尾插

index在(0,size):

  1. 找到前驱节点preNode
  2. 新节点next指向nextNode(此时用preNode.next表示)
  3. nextNode(此时新节点.next和preNode.next都可表示)的pre指向新节点
  4. preNode的next指向新节点
  5. 新节点的pre指向preNode

image-20231102000134083

头删除

头删除需要注意的就是删除不为空时候头删除只和head节点有关

head不为null:

  1. head = head.next 表示头指针指向下一个节点
  2. head 如果不为null(有可能就一个节点),head.pre = null 断掉与前一个节点联系 ;head如果为null,说明之前就一个节点head和pre都指向第一个节点,此时需要设置tail为null。

image-20231102002747786

尾删除

尾删除和头删除类似,考虑好tail节点情况

如果tail不为null:

  1. tail = tail.pre
  2. 如果tail不为null,那么tail.next = null 表示删除最后一个,如果tail为null,说明之前head和tail都指向一个唯一节点,这时候需要head = null。
编号删除

编号删除和编号插入类似,先考虑是否为头尾操作,然后再进行正常操作。

index为0:调用头删

index为size:调用尾删

index在(0,size):

  1. 找到待删除节点current
  2. 前驱节点(current.pre)的next指向后驱节点(current.next)
  3. 后驱节点的pre指向前驱节点

image-20231102075437513

完整代码

根据上面的流程,实现一个不带头结点的双链表,在查找方面,可以根据靠头近还是尾近,选择从头或者尾开始遍历。

代码:

/*
 * 不带头节点的
 */
package code.linearStructure;

/**
 * @date 2023.11.02
 * @author bigsai
 * @param <T>
 */
public class DoubleLinkedList<T> {

    private Node<T> head;
    private Node<T> tail;
    private int size;

    public DoubleLinkedList() {
        this.head = null;
        this.tail = null;
        size = 0;
    }

    // 在链表头部添加元素
    public void addHead(T data) {
        Node<T> newNode = new Node<>(data);
        if (head == null) {
            head = newNode;
            tail = newNode;
        } else {
            newNode.next = head;
            head.pre = newNode;
            head = newNode;
        }
        size++;
    }

    // 在指定位置插入元素
    public void add(T data, int index) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Index is out of bounds");
        }

        if (index == 0) {
            addHead(data);
        } else if (index == size) {
            addTail(data);
        } else {
            Node<T> newNode = new Node<>(data);
            Node<T> preNode = getNode(index-1);
            //step 1 2 新节点与后驱节点建立联系
            newNode.next = preNode;
            preNode.next.pre = newNode;
            //step 3 4 新节点与前驱节点建立联系
            preNode.next = newNode;
            newNode.pre = preNode;
            size++;
        }
    }

    // 在链表尾部添加元素
    public void addTail(T data) {
        Node<T> newNode = new Node<>(data);
        if (tail == null) {
            head = newNode;
            tail = newNode;
        } else {
            newNode.pre = tail;
            tail.next = newNode;
            tail = newNode;
        }
        size++;
    }

    // 删除头部元素
    public void deleteHead() {
        if (head != null) {
            head = head.next;
            if (head != null) {
                head.pre = null;
            } else { //此时说明之前head和tail都指向唯一节点,链表删除之后head和tail都应该指向null
                tail = null;
            }
            size--;
        }
    }

    // 删除指定位置的元素
    public void delete(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index is out of bounds");
        }

        if (index == 0) {
            deleteHead();
        } else if (index == size - 1) {
            deleteTail();
        } else {
            Node<T> current = getNode(index);
            current.pre.next = current.next;
            current.next.pre = current.pre;
            size--;
        }
    }

    // 删除尾部元素
    public void deleteTail() {
        if (tail != null) {
            tail = tail.pre;
            if (tail != null) {
                tail.next = null;
            } else {//此时说明之前head和tail都指向唯一节点,链表删除之后head和tail都应该指向null
                head = null;
            }
            size--;
        }
    }

    // 获取指定位置的元素
    public T get(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index is out of bounds");
        }
        Node<T> node = getNode(index);
        return node.data;
    }

    // 获取链表的大小
    public int getSize() {
        return size;
    }

    private Node<T> getNode(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index is out of bounds");
        }

        if (index < size / 2) {
            Node<T> current = head;
            for (int i = 0; i < index; i++) {
                current = current.next;
            }
            return current;
        } else {
            Node<T> current = tail;
            for (int i = size - 1; i > index; i--) {
                current = current.pre;
            }
            return current;
        }
    }

    private static class Node<T> {
        T data;
        Node<T> pre;
        Node<T> next;

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

结语

在插入删除的步骤,很多人可能因为繁琐的过程而弄不明白,这个操作的写法可能是多样的,但本质操作都是一致的,要保证能成功表示节点并操作,这个可以画个图一步一步捋一下,看到其他不同版本有差距也是正常的。

还有很多人可能对一堆next.next搞不清楚,那我教你一个技巧,如果在等号右侧,那么它表示一个节点,如果在等号左侧,那么除了最后一个.next其他的表示节点。例如node.next.next.next可以看成(node.next.next).next。

在做数据结构与算法链表相关题的时候,不同题可能给不同节点去完成插入、删除操作。这种情况操作时候要谨慎先后顺序防止破坏链表结构。

系列仓库地址:https://github.com/javasmall/bigsai-algorithm
csdn专栏:数据结构与算法

原创不易,还请三连支持一下!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

lua 时间差功能概略

简介 在进行程序设计过程中&#xff0c;经常需要对某些函数、某些程序片断从开始运行到运行结束所耗费的时间进行一些量化。这种量化实际上就是计算时间差。 获取函数耗时情景如下&#xff1a; function time_used() --开始计时-- do something at here. --结束计时--时间差&…

交易所开发搭建

在当今的数字货币市场中&#xff0c;交易所开发搭建已经成为了一个重要的领域。交易所是数字货币交易的主要场所&#xff0c;为投资者提供了安全、可靠、高效的交易服本务文。将详细介绍交易所开发搭建的整个流程&#xff0c;包括需求分析、设计、技术选型、开发、测试和上线等…

【2】Spring Boot 3 项目搭建

目录 【2】Spring Boot 3 初始项目搭建项目生成1. 使用IDEA商业版创建2. 使用官方start脚手架创建 配置与启动Git版本控制 个人主页: 【⭐️个人主页】 需要您的【&#x1f496; 点赞关注】支持 &#x1f4af; 【2】Spring Boot 3 初始项目搭建 项目生成 1. 使用IDEA商业版创…

【Element】隐藏 el-table 展开行的箭头

需求 点击行展开行&#xff0c;隐藏箭头 方法 首先需求是点击行显示展开行 row-click"rowClick"const rowClick (row: any, column: any, event: any) > {console.log(row, column, event)if (multipleTable.value) {multipleTable.value.toggleRowExpansio…

PostgreSQL 技术内幕(十一)位图扫描

扫描算子在上层计算和底层存储之间&#xff0c;向下扫描底层存储的数据&#xff0c;向上作为计算的输入源&#xff0c;在SQL的执行层中&#xff0c;起着关键的作用。顺序、索引、位图等不同类型的扫描算子适配不同的数据分布场景。然而&#xff0c;扫描算子背后的实现原理是怎样…

投资自己,成就未来——人大女王金融硕士助力您成为金融领域的佼佼者

在这个日新月异的时代&#xff0c;金融行业的发展日益繁荣&#xff0c;对于金融人才的需求也越来越大。为了应对这一挑战&#xff0c;越来越多的人选择投身金融领域&#xff0c;提升自己的专业素养。而中国人民大学女王金融硕士项目&#xff0c;正是为了满足这一需求而设立的&a…

JVM在线分析-解决问题的工具一(jinfo,jmap,jstack)

1. jinfo (base) PS C:\Users\zishi\Desktop> jinfo Usage:jinfo <option> <pid>(to connect to a running process)where <option> is one of:-flag <name> to print the value of the named VM flag #输出对应名称的参数-flag [|-]<n…

Pandas数据预处理Pandas合并数据集在线闯关_头歌实践教学平台

Pandas数据预处理合并数据集 第1关 Concat与Append操作第2关 合并与连接第3关 案例&#xff1a;美国各州的统计数据 第1关 Concat与Append操作 任务描述 本关任务&#xff1a;使用read_csv()读取两个csv文件中的数据&#xff0c;将两个数据集合并&#xff0c;将索引设为Ladder…

element ui:常用的组件使用情况记录

前言 将element ui使用过程中一些常用的组件使用情况记录如下 组件 el-tree树组件 树父子节点成一列显示 没有进行设置之前显示效果 设置之后显示效果 ​​​​ 主要代码如下 <el-treeicon-class"none"expand-on-click-node"false"style"…

震裕科技-300953 三季报分析(20231108)

震裕科技-300953 基本情况 公司名称&#xff1a;宁波震裕科技股份有限公司 A股简称&#xff1a;震裕科技 成立日期&#xff1a;1994-10-18 上市日期&#xff1a;2021-03-18 所属行业&#xff1a;专用设备制造业 周期性&#xff1a;0 主营业务&#xff1a;精密级进冲压模具及下游…

Word通过Adobe打印PDF时总是报错,打开记事本

Word文档打印&#xff0c;选择Adobe作为打印机&#xff0c;打印过程中总是报错&#xff0c;不断打开记事本&#xff0c;提示打印出错&#xff0c;错误信息如下&#xff1a; %%[ ProductName: Distiller ]%% %%[Page: 1]%% %%[Page: 2]%% %%[ Error: invalidfont; OffendingCom…

Scala中编写多线程爬虫程序并做可视化处理

在Scala中编写一个爬虫程序来爬取店铺商品并进行可视化处理&#xff0c;需要使用Selenium和Jsoup库来操作网页。在这个例子中&#xff0c;我们将使用多线程来提高爬取速度。 1、首先&#xff0c;我们需要引入所需的库&#xff1a; import org.openqa.selenium.By import org.o…

【Unity之UI编程】在Unity中如何打图集,来降低DrowCall

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;UI_…

Mysql 不同存储引擎数据文件的形式详解

目录 MyISAM MERGE InnoDB Memory Archive CSV BLACKHOLE MySQL 中的每一个数据表在磁盘上至少被表示为一个文件&#xff0c;即存放着该数据表结构定义的 .frm 文件。不同的存储引擎还有其它用来存放数据和索引信息的文件。 从 MySQL 8.0 版本开始&#xff0c;frm 表结构…

[HCTF 2018]WarmUp全网最详细解释

查看源码找到提示 访问source.php 代码审计&#xff1a; class emmm{public static function checkFile(&$page){$whitelist ["source">"source.php","hint">"hint.php"]; 定义了一个名为emmm的类&#xff0c;在该类中有…

Linux之IPC通信共享内存与消息队列、管道、信号量、socket内存拷贝实例总结(六十二)

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

射频功率放大器应用中GaN HEMT的表面电势模型

标题&#xff1a;A surface-potential based model for GaN HEMTs in RF power amplifier applications 来源&#xff1a;IEEE IEDM 2010 本文中的任何第一人称都为论文的直译 摘要&#xff1a;我们提出了第一个基于表面电位的射频GaN HEMTs紧凑模型&#xff0c;并将我们的工…

ChatGPT如何管理对话历史?

问题 由于现在开始大量使用ChatGPT对话功能&#xff0c;认识到他在提供启发方面具有一定价值。比如昨天我问他关于一个微习惯的想法&#xff0c;回答的内容还是很实在&#xff0c;而且能够通过他的表达理解自己的问题涉及到的领域是什么。 此外&#xff0c;ChatGPT能够总结对话…

96 前缀树Trie

前缀树 题解1 STL题解2 参考官方 Trie&#xff08;发音类似 “try”&#xff09;或者说 前缀树 是一种树形数据结构&#xff0c;用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景&#xff0c;例如自动补完和拼写检查。 请你实现 Trie 类&#xff1a; …