数据结构与算法之双向链表的设计与实现

news2024/11/24 10:25:30

文章目录

  • 前言
  • 一、双向链表
    • 1.1 概念
    • 1.2 双向链表的应用
    • 1.3 双向链表的node方法
    • 1.4 双向链表的add方法
    • 1.5 双向链表的remove方法
    • 1.6 整体代码
    • 1.7 接口测试
  • 二、对比学习
    • 2.1 单向链表 vs 双向链表
    • 2.2 双向链表 vs 动态数组
    • 2.3 ArrayList和LinkedList的区别

前言

文章链接之前所介绍的是单向链表,查找元素只能从头节点开始寻找,判断出符合条件的元素,时间复杂度为O(n)。当链表节点数目过多时,查询性能下降。而有了双向链表后,我们可以从两个方向查询元素,提升查询效率。

一、双向链表

1.1 概念

双向链表是一种数据结构,由若干个节点构成,其中每个节点均由三部分构成,分别是前驱节点,元素,后继节点。双向链表中的节点在内存中是游离状态存在的,如下图所示:
在这里插入图片描述

1.2 双向链表的应用

1.1 Java集合框架中LinkedList底层就是通过双向链表实现的,我们可以通过查看,阅读源码进行分析。
1.2 MySQL的Innodb存储引擎管理的数据页可以组成一个双向链表,而每个数据页中的记录会按照主键值从小到大的顺序组成一个 单向链表。如下图所示:
在这里插入图片描述

1.3 双向链表的node方法

查询方式:对半查找,若查找的位置小于链表长度的一半,则从头结点开始顺序查找;否则,从尾结点开始逆序查找,这样做可以提高查询效率。

//根据索引找到节点(查找目标节点位置小于链表长度的一半就从前往后找,大于的话从后往前找话)
private Node<E> node(int index) {
    rangeCheck(index);
    Node<E> node;
    if (index < (size >> 1)){
        node = first;
        for (int i = 0; i < index; i++) {
            node = node.next;
        }
    }else {
        node = last;
        for (int i = size - 1; i > index; i--) {
            node = node.prev;
        }
    }
    return node;
}

1.4 双向链表的add方法

之前单项链表新增节点操纵需要获取到 index 前面的节点,现在双向链表不需要,可以通过 prev 获取到前驱节点,next 获取到后继节点。

add(E) -- 在链表尾部添加元素,将元素封装到节点中,创建新节点,让新节点和前一个节点建立双向链表的关系。
add(int index,E e) -- 在指定位置插入元素,其过程实际上就是断开链,重新构建链的过程。

在这里插入图片描述
双向链表只有一个节点的时候,如下图所示:
在这里插入图片描述

@Override
public void add(int index, E element) {
    rangeCheckForAdd(index);
    //一开始index == 0 size() == 0 则 old.next == null
    if (size == index){//往最后面添加元素
        //获取到最后一个节点
        Node<E> old = last;
        //创建新的尾节点,last指向新的尾节点
        last = new Node<>(last, element, null);
        if (old == null){
            first = last;
        }else {
            //之前的尾节点的next指向新的尾节点
            old.next = last;
        }
    }else {
        //获取指定index的节点
        Node<E> next = node(index);
        //获取指定index的节点前一个节点
        Node<E> prev = next.prev;
        //创建新节点并指定他的prev和next
        Node<E> node = new Node<>(prev, element, next);
        next.prev = node;
        if (prev == null){
            first = node;
        }else{
            prev.next = node;
        }
    }
    size++;
}

1.5 双向链表的remove方法

remove(int index) – 删除指定位置的元素,其过程实际上依然是断开链,重新构建链的过程。

@Override
public E remove(int index) {
    rangeCheck(index);
    //获取index位置的节点
    Node<E> node = node(index);
    //获取index位置的上一个节点
    Node<E> prev = node.prev;
    //获取index位置的下一个节点
    Node<E> next = node.next;
    //prev == null 则说明删除的是第一个节点 0位置节点
    if (prev == null){
        first = next;
    }else {
        prev.next = next;
    }
     //next == null 则说明删除的是最后一个节点 size()位置节点
    if (next == null){
        last = prev;
    }else{
        next.prev = prev;
    }
    size--;
    return node.element;
}

1.6 整体代码

package com.hbx.linkedList.bidirection;

import com.hbx.module.AbstractList;

public class LinkedList<E> extends AbstractList<E> {

    private Node<E> first;
    private Node<E> last;

    // 链表中的节点
    private static class Node<E> {
        E element; // 节点元素
        Node<E> prev; // 节点指向下一个节点
        Node<E> next; // 节点指向下一个节点

        public Node(Node<E> prev, E element, Node<E> next) {
            this.prev = prev;
            this.element = element;
            this.next = next;
        }

        @Override
        public String toString(){
            StringBuilder sb = new StringBuilder();
            if(prev != null){
                sb.append(prev.element);
            }else{
                sb.append("null");
            }
            sb.append("_").append(element).append("_");
            if(next != null){
                sb.append(next.element);
            }else{
                sb.append("null");
            }

            return sb.toString();
        }
    }

    @Override
    public void clear() {
        size = 0;
        //jvm的gcRoots(可达性分析)
        first = null;
        last = null;
    }

    @Override
    public E get(int index) {
        return node(index).element;
    }
    /**
     * 根据索引找到节点
     */
    private Node<E> node(int index) {
        rangeCheck(index);
        if (index < (size >> 1)) { // 索引小于一半从前往后找
            Node<E> node = first;
            for (int i = 0; i < index; i++) {
                node = node.next;
            }
            return node;
        } else { // 索引大于一半从后往前找
            Node<E> node = last;
            for (int i = size - 1; i > index; i--) {
                node = node.prev;
            }
            return node;
        }
    }

    @Override
    public E set(int index, E element) {
        /*
         * 最好:O(1)
         * 最坏:O(n)
         * 平均:O(n)
         */
        E old = node(index).element;
        node(index).element = element;
        return old;
    }

    @Override
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        //一开始index == 0 size() == 0 old.next == null
        if (size == index){//往最后面添加元素
            Node<E> old = last;
            last = new Node<>(old, element, null);
            if (old == null){
                first = last;
            }else {
                old.next = last;
            }
        }else {
            Node<E> next = node(index);
            Node<E> prev = next.prev;
            Node<E> node = new Node<>(prev, element, next);
            next.prev = node;
            if (prev == null){
                first = node;
            }else{
                prev.next = node;
            }
        }
        size++;
    }

    @Override
    public E remove(int index) {
        rangeCheck(index);
        Node<E> node = node(index);
        Node<E> prev = node.prev;
        Node<E> next = node.next;
        if (prev == null){
            first = next;
        }else {
            prev.next = next;
        }
        if (next == null){
            last = prev;
        }else{
            next.prev = prev;
        }
        size--;
        return node.element;
    }

    @Override
    public int indexOf(E element) {
        // 有个注意点, 如果传入元素为null, 则不能调用equals方法, 否则会空指针
        // 因此需要对元素是否为null做分别处理
        Node<E> node = first;
        if (element == null) {
            for (int i = 0; i < size; i++) {
                if (node.element == null)
                    return i;
                node = node.next;
            }
        } else {
            for (int i = 0; i < size; i++) {
                if (node.element.equals(element))
                    return i;
                node = node.next;
            }
        }
        return ELEMENT_NOT_FOUND;
    }

    @Override
    public String toString() {
        StringBuilder string = new StringBuilder();
        string.append("[size=").append(size).append(", ");
        Node<E> node = first;
        for (int i = 0; i < size; i++) {
            if (i != 0) {
                string.append(", ");
            }
            string.append(node);
            node = node.next;
        }
        string.append("]");
        return string.toString();
    }

}

1.7 接口测试

@Test
public void test(){
    List<Integer> list = new LinkedList<>();
    list.add(11);
    list.add(22);
    list.add(33);
    list.add(44);
    list.add(0,55);//55, 11,22, 33, 44
    list.add(2,66);//55, 11, 66, 22, 33, 44
    list.add(list.size(),77);//55, 11, 66, 22, 33, 44, 77

    list.remove(0);
    list.remove(2);
    list.remove(list.size()-1);
    System.out.println(list);
}

在这里插入图片描述

二、对比学习

2.1 单向链表 vs 双向链表

粗略对比一下删除的操作数量:操作数量缩减了近一半
在这里插入图片描述

2.2 双向链表 vs 动态数组

动态数组:开辟、销毁内存空间的次数相对较少,但可能造成内存空间浪费(可以通过缩容解决)
双向链表:开辟、销毁内存空间的次数相对较多,但不会造成内存空间的浪费。

  • 如果频繁在尾部进行添加、删除操作,动态数组、双向链表均可选择。
  • 如果频繁在头部进行添加、删除操作,建议选择使用双向链表。
  • 如果有频繁的(在任意位置)添加、删除操作,建议选择使用双向链表。
  • 如果有频繁的查询操作(随机访问操作),建议选择使用动态数组。

2.3 ArrayList和LinkedList的区别

  1. ArrayList底层是数组实现的,LinkedList底层是双向链表,二者的数据结构是不同的。
  2. 因为数据结构不同,所以最终的性能是不同的;
    查询元素:
    ArrayList是根据下标查找元素,查询效率非常高,时间复杂度为O(1)。
    LinkedList中若查找头部/尾部的元素,其查询效率还是比较高的,但是若查找偏中间位置的元素,其查询效率是比较低下的。
    增删元素:
    ArrayList:若在尾部添加增删元素,此时性能可能会很高,在头部和中间位置进行增删操作,其效率都不是很高。
    LinkedList:若在头尾部分增删元素,此时性能很高,但是若在偏中间位置进行增删元素,此时性能不高的(因为增删,会先查询指定位置的节点,查询效率是低下的)。
    整体而言:
    ArrayList查询性能是高于LinkedList的,但是若LinkedList进行头尾查询,此时效率也是非常高的。
    若进行的是偏头部和尾部的增删操作,选择LinkedList,而若对其他位置进行增删,此时ArrayList和LinkedList的效率是差不多的。
    ArrayList查询方面性能更好,增删方面,除了头尾增删,其他增删和LinkedList差不多,所以经常使用ArrayList。
    面试标准回答:
    ArrayList查询性能更高,LinkedList进行头尾增删,性能很高,除此之外,其他增删,ArrayList和LinkedList差不多。

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

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

相关文章

基于python的C环境安装(NLP文本纠错项目使用)

1.下载c环境&#xff1a;&#xff08;window系统&#xff09; 链接&#xff1a;Visual Studio: 面向软件开发人员和 Teams 的 IDE 和代码编辑器 (microsoft.com) 2.安装 1.打开下载的安装包 2.进入如下页面&#xff0c;按照下图进行勾选&#xff0c;注意&#xff0c;其它不要动…

全渠道营销与多渠道营销:定义、比较、示例

关键词&#xff1a;全渠道营销、多渠道营销 全渠道还是多渠道&#xff1f;您正在踏上跨境电子商务之旅&#xff0c;为您的品牌寻找合适的营销策略&#xff0c;但这一切似乎都过于理论化和复杂。 我们将使事情变得更容易&#xff0c;因为本文全面解释了多渠道营销和全渠道营销之…

【文本检测】1、DBNet | 实时场景文本检测器

文章目录一、背景二、方法2.1 二值化2.2 Adaptive threshold2.3 可变形卷积2.4 生成标签2.5 优化过程三、效果3.1 实验数据3.2 实验细节3.3 消融实验3.4 和其他方法的对比论文&#xff1a;Real-time Scene Text Detection with Differentiable Binarization 代码&#xff1a;h…

不懂应该怎么选合适的医疗器械进销存?

在医院运行过程中&#xff0c;需要管理医疗设备的采购、养护、报废等各个环节。医疗器械进销存软件是集医院设备、物资、耗材的申请、采购、出入库、维修、维护、折旧、固定资产管理、效益分析等全流程管理功能于一体&#xff0c;实现医院医疗设备的信息化&#xff0c;数据库规…

数据结构之【时间复杂度和空间复杂度】

如何去评价一个代码它的效率高不高呢&#xff1f; 我们通常从两个方面去看&#xff01; 时间复杂度&#xff1a;主要衡量一个算法的运行速度空间复杂度&#xff1a;主要衡量一个算法所需要的额外空间 1. 时间复杂度 1.1 时间复杂度的定义 在计算机科学中&#xff0c;算法的…

算法题中常用的位运算

文章目录为什么使用位运算&#xff1f;十进制和二进制之间的转化短除法&#xff08;十进制转二进制&#xff09;幂次和&#xff08;二进制转十进制&#xff09;位运算符异或运算&#xff08;xor&#xff09;指定位置的位运算位运算实战要点为什么使用位运算&#xff1f; 机器采…

代码随想录刷题记录day46 最长公共子序列+不相交的线+最大子数组和

代码随想录刷题记录day46 最长公共子序列不相交的线最大子数组和 1143. 最长公共子序列 思想 1.dp数组的定义 dp[i][j]表示 以i-1为结尾的字符串text1和以j-1为结尾的字符串2的最长公共子序列长度 2.递推公式 如果text1.charAt(i-1)text2.charAt(j-1) dp[i][j]dp[i-1][j-1…

TS 对象可能为“未定义”,不能将类型“ XXXX | undefined “分配给类型{ xxxx }

前言&#xff1a; 最近用 typeScript &#xff0c;也就是大家常说的 【 TS 】写点东西&#xff0c;但是老是提醒这个未定义&#xff0c;那个可能为空&#xff0c;主要是 tsconfig.json 中的严格模式我没关&#xff0c;所以今天总结一下&#xff0c;严格模式中【TS】中遇到 对象…

Learning Disentangled Label Representations for Multi-label Classification

Learning Disentangled Label Representations for Multi-label Classification&#xff0c;2022 学习多标签分类的解纠缠标签表示 要点&#xff1a; 1、主流多标签分类&#xff1a;遵循单标签&#xff08;多类别&#xff09;分类的特征学习机制——学习一个共享的图像特征来…

【Vue实践】尚硅谷张天禹Vue学习笔记(087-135)-20221212~20221218

&#xff08;任意组件通信&#xff09;084-086_全局事件总线 全局事件总线SOP 086_TodoList案例_事件总线 src/mian.js: import Vue from vue import App from ./App.vueVue.config.productionTip falsenew Vue({el:"#app",render: h > h(App),beforeCreate()…

docker高级篇第二章-分布式存储之实战案例:3主3从redis集群搭建

在上一篇文章中&#xff0c;我们介绍了分布式存储的三种方式&#xff1a;hash取余分区、一致性哈希算法分区以及哈希槽分区。本篇&#xff0c;我们就来实战3主3从的哈希槽Redis集群搭建。 大家好,我是凯哥Java(kaigejava)&#xff0c;乐于分享&#xff0c;每日更新技术文章&…

【Redis深度专题】「核心技术提升」分析探究如何实现LFU的热点key发现机制以及内部的Scan扫描技术的原理

前言介绍 业务中存在访问热点是在所难免的&#xff0c;redis也会遇到这个问题&#xff0c;然而如何发现热点key一直困扰着许多用户&#xff0c;redis4.0为我们带来了许多新特性&#xff0c;其中便包括基于LFU的热点key发现机制。 Least Frequently Used Least Frequently Us…

基于节点导纳矩阵的三相配电系统建模(Matlab实现)

目录 1 概述 2 算例仿真 2.1 IEEE 37节点测试 2.2 EEE 123 节点测试 2.3 500 节点测试 2.4 906 母线低压馈线 2.5 小节 3 Matlab代码实现 1 概述 本文的主要是适用于 Z-Bus 潮流的三相配电系统建模。提供了星形和三角形恒功率、恒电流和恒阻抗负载的详细模型。布置了…

无线投屏(智慧教室)

大家好&#xff0c;我是小杜&#xff0c;打工人又开始了这一周的“搬砖”了。周末两天很好的“休息”后&#xff0c;今天浑身充满了干劲&#xff0c;都可以打死一头“牛”&#xff0c;从今天开始就要参与公司的一些业务了&#xff0c;剩余时间就是打工人最喜欢的学习时间了。 …

Nacos系列——Java SDK(2.x版本)2-1

Nacos系列——Java SDK&#xff08;2.x版本&#xff09;2-1资源地址README概述&#xff08;intro&#xff09;Nacos Java SDK 官方文档(official doc address)工程说明&#xff08;project intro &#xff09;工程目录&#xff08;project dir&#xff09;pom依赖(pom dependen…

面向切面编程

Spring AOP简介 AOP把业务功能分为核心、非核心两部分。 核心业务功能&#xff1a;用户登录、增加数据、删除数据。非核心业务功能&#xff1a;性能统计、日志、事务管理。 在Spring的面向切面编程&#xff08;AOP&#xff09;思想里&#xff0c;非核心业务功能被定义为切面。…

Springboot+echarts:ajax前后端分离交互

文章目录一、样例说明二、后端代码实现2.1 依赖2.2 applicaiton.properties配置2.3 TotalCountData类实现2.4 totalCountDataMapper接口2.5 totalCountDataMapper.xml实现2.6 Controller层代码三、前端代码一、样例说明 通过mysql存储数据&#xff0c;springboot整合mybatis框…

从云到「链」,京东云成为中国第四朵云背后

在产业加速到数实融合加速的今年&#xff0c;云计算不再是云厂商的唯一考校指标。 作者|叶子 出品|产业家 京东云再次破圈。 信号来自接连发布的几份报告。在国际权威研究机构Forrester发布的名为《The Forrester Wave&#xff1a;Public Cloud Development And Infrast…

[HCTF 2018]WarmUp

目录 考点 writeup 考点 文件上传漏洞&#xff0c;代码审计 writeup 先进入页面先查看源码 发现source.php,打开该php文件&#xff0c;进行审计代码后发现是文件包含类题目 <?phphighlight_file(__FILE__);class emmm{public static function checkFile(&$page){$…

JaveWeb框架(三):实战项目Servlet 实现管理系统登录注册功能

MVC实战项目 仓储管理系统需求&#xff1a;实现基本的登录和注册功能MVC实战项目&#xff1a;登录和注册登录功能实现注册功能实现总结Redis章节复习已经过去&#xff0c;新的章节JavaWeb开始了&#xff0c;这个章节中将会回顾JavaWeb实战项目 仓储管理 代码会同步在我的gitee中…