【数据结构与算法】5.详解双向链表的基本操作(Java语言实现)

在这里插入图片描述
📚博客主页:爱敲代码的小杨.

✨专栏:《Java SE语法》

❤️感谢大家点赞👍🏻收藏⭐评论✍🏻,您的三连就是我持续更新的动力❤️

🙏小杨水平有限,欢迎各位大佬指点,相互学习进步!

文章目录

  • 0. 前言
  • 1. 双链表的定义
  • 2. LinkedList 模拟实现
    • 2.1 接口
    • 2.2 定义双向链表类
    • 2.3 定义两个指针,分别指向头节点和尾节点
    • 2.4 头插法
    • 2.5 尾插法
    • 2.6 指定位置插入元素
    • 2.7 查找指定元素
    • 2.8 删除指定元素
    • 2.9 删除链表中所有指定元素
    • 2.10 统计链表元素个数
    • 2.11 清空链表
    • 2.12 打印链表
    • 2.13 测试
  • 3.代码
  • 4. ArrayList和LinkedList的区别

0. 前言

上一篇【数据结构与算法】4.自主实现单链表的增删查改 我们自主实现了单链表的操作,在Java的集合类中LinkedList底层实现是无头双向循环链表。所以今天我们模拟LinkedList的实现。

1. 双链表的定义

学习双链表之前,做个回顾。

单链表的特点:

  1. 我们可以轻松的到达下一个节点,但是回到前一节点是很难的。
  2. 只能从头遍历到尾或者从尾遍历到头(一般是从头到尾)

双链表的特点:

  1. 每次在插入或删除某个节点时, 需要处理四个节点的引用, 而不是两个. 实现起来要困难一些
  2. 相对于单向链表, 必然占用内存空间更大一些.
  3. 既可以从头遍历到尾, 又可以从尾遍历到头

双链表的定义:

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

image-20240125171130806

指针域(prev):用于指向当前节点的直接前驱节点;

数据域(data):用于存储数据元素;

指针域(next):用于指向当前节点的直接后继节点。

2. LinkedList 模拟实现

2.1 接口

public interface IList {
    //头插法
    public void addFirst(int data);

    //尾插法
    public void addLast(int data);

    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data);

    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key);

    //删除第一次出现关键字为key的节点
    public void remove(int key);

    //删除所有值为key的节点
    public void removeAllKey(int key);

    //得到单链表的长度
    public int size();

    // 清空链表
    public void clear();

    // 打印链表
    public void display();
}

2.2 定义双向链表类

static class ListNode {
	public int val; // 数值域 - 存放当前节点的值
	public ListNode next; // next域 指向下一个节点
	public ListNode prev; // prev域 指向上一个节点

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

2.3 定义两个指针,分别指向头节点和尾节点

// 链表的属性 链表的头节点
public ListNode head; 
// 链表的属性 链表的尾节点
public ListNode last;

2.4 头插法

  1. 判断链表是否为空,如果为空,将新节点的node设置为头节点,将新节点的node设置为尾节点

    head = node;
    last = node;
    
  2. 如果链表不为空,将新节点的nodenext域设置为头节点,将当前头节点的prev设置为新节点的node,更新头节点为新节点的node

    node.next = head;
    head.prev = node;
    head = node;
    

动画演示:

1706271497900

代码:

/**
* 头插法
* @param data
*/
@Override
public void addFirst(int data) {
	ListNode node = new ListNode(data);
	if (head == null) {
		head = node;
		last = node;
	}else {
		node.next = head;
		head.prev = node;
		head = node;
	}
}

2.5 尾插法

  1. 判断链表是否为空,如果为空,将新节点的node设置为头节点,将新节点的node设置为尾节点

    head = node;
    last = node;
    
  2. 如果链表不为空,将最后一个节点lastnext域指向新节点,新节点的prev域指向最后一个节点,更新尾节点为新节点

    last.next = node;
    node.prev = last;
    last = node;
    

动画演示:

1706271820869

代码:

    /***
     * 尾插法
     * @param data
     */
    @Override
    public void addLast(int data) {
        ListNode node = new ListNode(data);
        if (head == null) {
            head = node;
            last = node;
        } else {
            last.next = node;
            node.prev = last;
            last = node;
        }
    }

2.6 指定位置插入元素

  1. 判断索引idnex是否合法,如果不合法则抛出异常。

    if (index < 0 || index > size()) {
    	throw new IndexException("index不合法:" + index);
    }
    
  2. 判断链表是否为空,如果为空则将新节点设置为头节点和尾节点

    if (head == null) {
    	head = node;
        last = node;
        return;
    }
    
  3. 如果索引index == 0,则使用头插法,如果索引index = 链表长度,则使用尾插法

    if (index == 0) {
        addFirst(data);
    	return;
    }
    if (index == size()) {
        addLast(data);
    	return;
    }
    
  4. 找到索引节点(当前节点)

        private ListNode findIndex(int index) {
            ListNode cur = head;
            while (index != 0) {
                cur = cur.next;
                index--;
            }
            return cur;
        }
    
  5. 将新节点的next域指向当前节点,新节点的prev域指向当前节点的前一个节点,当前节点的prev域指向新节点,更新新节点的上一个节点的next域指向当前节点。

    ListNode cur = findIndex(index);
    node.next = cur;
    node.prev = cur.prev;
    cur.prev = node;
    node.prev.next = node;
    

动画演示:

1706272997346

2.7 查找指定元素

  1. 从头节点开始遍历链表,如果当前节点的值与要查找的key相等,则返回ture,如果不相等则移动下一个节点继续查找。如果遍历完链表都没有找到key则返回false.

代码:

    @Override
    public boolean contains(int key) {
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

2.8 删除指定元素

  1. 从头节点开始遍历链表,找到要删除的节点

  2. 情况一:删除的节点为头节点,更新头节点为下一个节点,更新下一个节点的prev域置为空。

    1706277210409

  3. 情况二:链表中只有一个元素,且正好要删除这个元素。

  4. 情况三:删除的节点为尾节点,更新尾节点为当前节点的上一个节点,上一个节点的next域置为空

    1706277783850

  5. 情况四:删除中间节点,当前节点的上一个节点的next域指向当前节点的下一个节点,更新下一个节点的prev域指向当前节点的上一个节点

    1706278104977

  6. 删除了节点就结束方法的执行

代码:

    @Override
    public void remove(int key) {
         ListNode cur = head;
         while (cur != null) {
             if (cur.val == key) { // 找到要删除的元素了
                 if (cur == head) { // 删除头节点
                     head = head.next;
                     if (head != null) {
                         head.prev = null;
                     } else { // 链表中只有一个元素,且这个正好删除这个元素
                         last = null;
                     }
                 } else { // 删除中间节点
                     cur.prev.next = cur.next;
                     if (cur.next != null) {
                        cur.next.prev = cur.prev;
                     } else {
                         // 删除尾节点
                         last = cur.prev;
                     }
                 }
                 return;// 删除了节点就结束方法
             }
             cur = cur.next;
         }
    }

2.9 删除链表中所有指定元素

从头节点遍历链表,与删除指定元素的方式一样,删除节点后继续遍历链表,直到遍历完链表,删除所有指定的元素即可。

代码:

    @Override
    public void removeAllKey(int key) {
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == key) { // 找到要删除的元素了
                if (cur == head) { // 删除头节点
                    head = head.next;
                    if (head != null) {
                        head.prev = null;
                    } else { // 链表中只有一个元素,且这个正好删除这个元素
                        last = null;
                    }
                } else { // 删除中间节点
                    cur.prev.next = cur.next;
                    if (cur.next != null) {
                        cur.next.prev = cur.prev;
                    } else {
                        // 删除尾节点
                        last = cur.prev;
                    }
                }
            }
            cur = cur.next;
        }
    }

2.10 统计链表元素个数

代码:

    @Override
    public int size() {
        int count = 0;
        ListNode cur = head;
        while (cur != null) {
            count++;
            cur = cur.next;
        }
        return count;
    }

2.11 清空链表

将头节点和尾节点置为空,没有引用指向直接被JVM回收

    @Override
    public void clear() {
        head = null;
        last = null;
    }

2.12 打印链表

    @Override
    public void display() {
        ListNode cur = head;
        while (cur != null) {
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }

2.13 测试

public class Test {
    public static void main(String[] args) {
        MyLinkedList myLinkedList = new MyLinkedList();
        // 头插法
        myLinkedList.addFirst(1);
        myLinkedList.addFirst(2);
        myLinkedList.addFirst(3);
        // 打印链表
        myLinkedList.display();

        System.out.println("=========");

        // 尾插法
        myLinkedList.addLast(4);
        myLinkedList.addLast(5);
        myLinkedList.addLast(6);
        // 打印链表
        myLinkedList.display();

        System.out.println("=========");

        // 在4 位置插入7
        myLinkedList.addIndex(4,7);
        // 打印链表
        myLinkedList.display();

        System.out.println("=========");

        // 查找元素 7 8
        System.out.println(myLinkedList.contains(7));
        System.out.println(myLinkedList.contains(8));

        System.out.println("=========");

        // 删除3 6 4
        myLinkedList.remove(3);
        myLinkedList.display();
        System.out.println("=========");

        myLinkedList.remove(6);
        myLinkedList.display();
        System.out.println("=========");

        myLinkedList.remove(4);
        myLinkedList.display();
        System.out.println("=========");


        // 删除全部的2
        myLinkedList.addLast(2);
        myLinkedList.addLast(2);
        myLinkedList.addLast(2);
        myLinkedList.display();
        myLinkedList.removeAllKey(2);
        myLinkedList.display();

        System.out.println("=========");

        // 统计个数
        System.out.println(myLinkedList.size());

        System.out.println("=========");

        // 清空链表
        myLinkedList.clear();
        myLinkedList.display();
        System.out.println("=========");
        
        // 统计个数
        System.out.println(myLinkedList.size());
    }
}

// 运行结果
3 2 1 
=========
3 2 1 4 5 6 
=========
3 2 1 4 7 5 6 
=========
true
false
=========
2 1 4 7 5 6 
=========
2 1 4 7 5 
=========
2 1 7 5 
=========
2 1 7 5 2 2 2 
1 7 5 
=========
3
=========

=========
0

3.代码

MyLinkedList类:

public class MyLinkedList implements IList{

    static class ListNode {
        public int val; // 数值域 - 存放当前节点的值
        public ListNode next; // next域 指向下一个节点
        public ListNode prev; // prev域 指向上一个节点

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

    // 链表的属性 链表的头节点
    public ListNode head;
    // 链表的属性 链表的尾节点
    public ListNode last;

    /**
     * 头插法
     * @param data
     */
    @Override
    public void addFirst(int data) {
        ListNode node = new ListNode(data);
        if (head == null) {
            head = node;
            last = node;
        }else {
            node.next = head;
            head.prev = node;
            head = node;
        }
    }

    /***
     * 尾插法
     * @param data
     */
    @Override
    public void addLast(int data) {
        ListNode node = new ListNode(data);
        if (head == null) {
            head = node;
            last = node;
        } else {
            last.next = node;
            node.prev = last;
            last = node;
        }
    }

    @Override
    public void addIndex(int index, int data) {
        if (index < 0 || index > size()) {
            throw new IndexException("index不合法:" + index);
        }
        ListNode node = new ListNode(data);
        if (head == null) {
            head = node;
            last = node;
            return;
        }
        if (index == 0) {
            addFirst(data);
            return;
        }
        if (index == size()) {
            addLast(data);
            return;
        }
        ListNode cur = findIndex(index);
        node.next = cur;
        node.prev = cur.prev;
        cur.prev = node;
        node.prev.next = node;
    }

    private ListNode findIndex(int index) {
        ListNode cur = head;
        while (index != 0) {
            cur = cur.next;
            index--;
        }
        return cur;
    }

    @Override
    public boolean contains(int key) {
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

    @Override
    public void remove(int key) {
         ListNode cur = head;
         while (cur != null) {
             if (cur.val == key) { // 找到要删除的元素了
                 if (cur == head) { // 删除头节点
                     head = head.next;
                     if (head != null) {
                         head.prev = null;
                     } else { // 链表中只有一个元素,且这个正好删除这个元素
                         last = null;
                     }
                 } else { // 删除中间节点
                     cur.prev.next = cur.next;
                     if (cur.next != null) {
                        cur.next.prev = cur.prev;
                     } else {
                         // 删除尾节点
                         last = cur.prev;
                     }
                 }
                 return;
             }
             cur = cur.next;
         }
    }

    @Override
    public void removeAllKey(int key) {
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == key) { // 找到要删除的元素了
                if (cur == head) { // 删除头节点
                    head = head.next;
                    if (head != null) {
                        head.prev = null;
                    } else { // 链表中只有一个元素,且这个正好删除这个元素
                        last = null;
                    }
                } else { // 删除中间节点
                    cur.prev.next = cur.next;
                    if (cur.next != null) {
                        cur.next.prev = cur.prev;
                    } else {
                        // 删除尾节点
                        last = cur.prev;
                    }
                }
            }
            cur = cur.next;
        }
    }

    @Override
    public int size() {
        int count = 0;
        ListNode cur = head;
        while (cur != null) {
            count++;
            cur = cur.next;
        }
        return count;
    }

    @Override
    public void clear() {
        head = null;
        last = null;
    }

    @Override
    public void display() {
        ListNode cur = head;
        while (cur != null) {
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }
}

接口:

public interface IList {
    //头插法
    public void addFirst(int data);

    //尾插法
    public void addLast(int data);

    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data);

    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key);

    //删除第一次出现关键字为key的节点
    public void remove(int key);

    //删除所有值为key的节点
    public void removeAllKey(int key);

    //得到单链表的长度
    public int size();

    // 清空链表
    public void clear();

    // 打印链表
    public void display();
}

异常类:

public class IndexException extends RuntimeException{
    public IndexException() {

    }
    public IndexException(String msg) {
        super(msg);
    }
}

4. ArrayList和LinkedList的区别

不同点ArrayListLinkedList
存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
随机访问支持O(1)不支持O(n)
头插需要搬移元素,效率低O(n)只需要修改引用的指向,时间复杂度为O(1)
插入空间不够时需要扩容没有容量的概念
应用场景元素高效存储 + 频繁访问任意位置插入和删除频繁

在这里插入图片描述

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

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

相关文章

Redis学习——入门篇③

Redis学习——入门篇③ 1. Redis事务1.1 事务实际操作1.2 watch 2. Redis管道&#xff08;pipelining&#xff09;2.1 管道简介2.2 管道实际操作2.3 管道小总结 3. Redis&#xff08;pub、sub&#xff09;发布订阅(不重要)3.1 简介3.2 发布订阅实际操作 这是一个分水岭…

Datawhale 组队学习之大模型理论基础 Task7 分布式训练

第8章 分布式训练 8.1 为什么分布式训练越来越流行 近年来&#xff0c;模型规模越来越大&#xff0c;对硬件&#xff08;算力、内存&#xff09;的发展提出要求。因为内存墙的存在&#xff0c;单一设持续提高芯片的集成越来越困难&#xff0c;难以跟上模型扩大的需求。 为了…

ChatGPT惊艳更新!一个@让三百万GPTs为你打工

ChatGPT悄悄更新个大功能&#xff01;看起来要把插件系统迭代掉了。 部分(灰度)用户已经收到这样的提示&#xff1a; 现在可以在对话中任意GPT商店里的GPTs&#xff0c;就像在群聊中一个人。 体验到的博主Dan Shipper第一时间录视频激动地分享&#xff1a;一个改变游戏规则的…

Mediasoup Demo-v3笔记(七)——Mediasoup 下的业务操作

Mediasoup 主业务的创建流程 Mediasoup 权限验证的创建 在实际的代码中&#xff0c;在main.cpp 中调用mediasoup_worker_run函数 在mediasoup_worker_run函数实现中进行了各个模块的初始化 Mediasoup 数据流转 创建连接的过程 数据传输过程 WebRTC大规模部署方案 方案一…

电脑 文件夹内 显示是 文件在一起 ,实际存储硬盘的不同地方?

是的&#xff0c;在电脑上&#xff0c;文件夹内显示在一起的文件可能实际上存储在硬盘的不同物理位置。这是因为现代操作系统使用的是文件系统来管理磁盘上的数据&#xff0c;常见的如NTFS&#xff08;Windows&#xff09;、HFS&#xff08;旧版Mac&#xff09;或APFS&#xff…

FreeRtos Queue (二)

本篇主要讲Queue的prvLockQueue和prvUnlockQueue 一、前言 1、prvLockQueue和prvUnlockQueue是FreeRtos内核函数&#xff0c;只能供内核调用&#xff0c;应用层无法call。 2、cTxLock和cRxLock为中断上锁计数器&#xff0c;cTxLock记录了队列上锁期间在中断里入队的数量&#…

sqli-lbs靶场搭建

目录 环境小皮源码下载 1.源码解压&#xff1a; 2.搭建网站 2.1点击创建网站 2.2修改sql-connections\db-creds.inc 2.3重新启动 3.访问你设置的域名 3.1点击启动数据库配置 3.2返回第一个页面&#xff08;开启题目&#xff09; sqlilbs靶场搭建 环境小皮源码下载 下载地址&am…

RuoYi-Vue前后端分离后台开发框架运行详细教程

一、官网下载代码 RuoYi-Vue是一款基于SpringBootVue的前后端分离极速后台开发框架。 若依官网&#xff1a;http://ruoyi.vip演示地址&#xff1a;http://vue.ruoyi.vip代码下载&#xff1a;https://gitee.com/y_project/RuoYi-Vue 下载之后解压&#xff0c;ruoyi-ui是前端代…

Vulnhub靶场DC-3

本机192.168.223.128 靶机192.168.223.139 目标发现nmap -sP 192.168.223.0/24 端口扫描nmap -p- 192.168.223.139 之开启了一个80端口 看一下是什么服务 nmap -sV -p- -A 192.168.223.139是一个apache服务&#xff0c;joomla模板 看一下web 没什么有用信息。 扫描一下后台…

如何监控两台android设备之间串口通讯的ADB日志?

如果你的目标是将设备通过 Wi-Fi 连接到计算机&#xff0c;可以执行以下步骤&#xff1a; 一.通过 USB 连接设备&#xff1a; adb devices 确保设备通过 USB 连接&#xff0c;并且可以通过 adb devices 命令正常识别。 二、将设备1和设备2都切换到 TCP/IP 模式&#xff1a;…

[java基础揉碎]for循环的细节

1.for循环中的初始化和变量迭代可以写到其它地方&#xff0c;但是两边的分号不能省略 2.可以不写循环变量的初始化, 控制和迭代, 直接写分号, 表示无限循环 3.循环初始值可以有多条初始化语句&#xff0c;但要求类型一样&#xff0c;并且中间用逗号隔开&#xff0c; 循环变量迭…

mac滚动截图

参考博客 https://www.zhihu.com/question/313673726/answer/2938671835 首先去AppStore搜索 iShot 这个也是要钱的&#xff0c;不过我输入appleID后&#xff0c;并没有扣我钱&#xff0c;不知道设么回事 然后打开iShot 点击观看视频&#xff0c;然后mac会自动打开一个新的…

【K8S】Kubernetes 中滚动发布由浅入深实战

目录 一、Kubernetes中滚动发布的需求背景1.1 滚动发布1.2 滚动发布、蓝绿发布、金丝雀发布的区别 二、Kubernetes中实现滚动发布2.1 定义Kubernetes中的版本2.2 创建 Deployment 资源对象2.2.1 在 Yaml 中定义 Deployment 资源对象2.2.2 执行命令创建 Deployment 资源对象 三、…

muduo库的模拟实现——muduo库的介绍

文章目录 一、muduo库介绍二、背景知识1.epoll2.Reactor模式 三、功能模块划分1.工具部分2.Reactor部分3.TCPServer部分 一、muduo库介绍 muduo库是在Linux环境下使用C实现的一个多Reactor多线程的高性能网络服务器&#xff0c;作者陈硕&#xff0c;他还出了一本书《Linux多线…

DB2数据库,时间类型插入数据

DB2数据库,时间类型插入数据 1、TIMESTAMP类型 1.1、创建表 CREATE TABLE BI_varchar ( id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1 ), hide_zero varchar(1000), simulation_cphone_de varchar(1000), disorder_de varchar(…

人工智能:更多有用的 Python 库

目录 前言 推荐 JupyterLab 入门 复杂的矩阵运算 其它人工智能和机器学习的 Python 库 前言 在这篇文章中&#xff0c;我们将了解更多的矩阵操作&#xff0c;同时再介绍几个人工智能 Python 库。 推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#x…

【机组】基于FPGA的32位算术逻辑运算单元的设计(EP2C5扩充选配类)

​&#x1f308;个人主页&#xff1a;Sarapines Programmer&#x1f525; 系列专栏&#xff1a;《机组 | 模块单元实验》⏰诗赋清音&#xff1a;云生高巅梦远游&#xff0c; 星光点缀碧海愁。 山川深邃情难晤&#xff0c; 剑气凌云志自修。 目录 一、实验目的 二、实验要求 …

幻兽帕鲁 Mac 怎么玩? 手把手教你下载CrossOver 玩 Palworld 幻兽帕鲁游戏

《幻兽帕鲁》&#xff08;英文&#xff1a;Palworld&#xff09;是一款近期在 Steam 爆红的动作冒险生存游戏&#xff0c;游戏设置在一个居住着「帕鲁」的开放世界中&#xff0c;玩家可以战斗并捕捉帕鲁&#xff0c;也能用它们来建造基地、骑乘和战斗。 不过目前《幻兽帕鲁》仅…

3d gaussian splatting笔记(paper部分翻译)

本文为3DGS paper的部分翻译。 基于点的&#x1d6fc;混合和 NeRF 风格的体积渲染本质上共享相同的图像形成模型。 具体来说&#xff0c;颜色 &#x1d436; 由沿射线的体积渲染给出&#xff1a; 其中密度 &#x1d70e;、透射率 &#x1d447; 和颜色 c 的样本是沿着射线以…

仅使用 Python 创建的 Web 应用程序(前端版本)第09章_购物车

在本章中,我们将实现购物车页面。 完成后的图像如下。 创建过程与之前相同,如下。 No分类内容1Model创建继承BaseDataModel的数据类Cart、CartItem2Service创建一个 CartAPIClient3Page定义PageId并创建继承自BasePage的页面类4Application将页面 ID 和页面类对添加到 Multi…