Java: LinkedList的模拟实现

 一、双向链表简介

上一篇文章我介绍了单向链表的实现,单向链表的特点是:可以根据上一个节点访问下一个节点!但是,它有个缺点,无法通过下一个节点访问上一个节点!这也是它称为单向链表的原因。

  那么,可不可以实现这样一种链表,它既可以通过一个节点,访问其上一个节点和下一个节点,也是可以的!它就是我接下来要介绍的双向链表

  如图:与单向链表不同的是,双向链表的每个节点包含三个域:val、pre、next,分别代表当前节点的值、上一个节点的地址、下一个节点的地址。



 

二、双向链表的实现

双向链表实现的全部代码:

 类文件:

 异常类代码:(IndexNotLegalException)

//自定义异常类:
public class IndexNotLegalException extends RuntimeException{
    public IndexNotLegalException() {
    }

    public IndexNotLegalException(String message) {
        super(message);
    }
}

双向链表实现代码:(MyLinkedList)

import javax.management.ListenerNotFoundException;
import java.util.List;

public class MyLinkedList {
    //创建ListNode内部类
    class ListNode {
        public int val;
        public ListNode pre;//前驱
        public ListNode next;//后继


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

    public ListNode head;//标志头节点
    public ListNode last;//标志尾节点


    //返回链表长度的方法:
    public int size() {
        int count = 0;
        ListNode cur = head;
        while (cur != null) {
            cur = cur.next;
            count++;
        }
        return count;
    }


    //打印链表的方法;
    public void display() {
        ListNode cur = head;
        while (cur != null) {
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }


    //查看是否波包含key关键字的方法:
    public boolean contains(int key) {
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }


    //头部插入的方法
    public void addFirst(int data) {
        ListNode node = new ListNode(data);
        //如果head为null
        if (head == null) {
            head = last = node;
        } else {
            head.pre = node;
            node.next = head;
            head = node;
        }

    }


    //尾部插入的方法:
    public void addLast(int data) {
        ListNode node = new ListNode(data);
        //如果head==null
        if (head == null) {
            head = last = node;
        } else {
            last.next = node;
            node.pre = last;
            last = node;
        }
    }


    //指定位置插入的方法:
    public void addIndex(int index, int data) {
        //1、判断index是否合法
        try {
            checkIndex(index);
        } catch (IndexNotLegalException e) {
            e.printStackTrace();
        }

        //index==0,相当于头部插入
        if (index == 0) {
            addFirst(data);
            return;
        }
        //index==size(),相当于尾部插入
        if (index == size()) {
            addLast(data);
            return;
        }
        //0<index<size()
        if (index > 0 && index < size()) {
            //找到下标为index的节点
            ListNode cur = findIndex(index);
            //连接节点
            ListNode node = new ListNode(data);
            node.next = cur;
            node.pre = cur.pre;
            cur.pre.next = node;
            cur.pre = node;
            return;

        }
    }

    //找到index节点的方法:
    public ListNode findIndex(int index) {
        int count = index;
        ListNode cur = head;
        while (count != 0) {
            cur = cur.next;
            count--;
        }
        return cur;
    }

    //检查index是否合法的方法
    private void checkIndex(int index) {
        if (index < 0 || index > size()) {
            throw new IndexNotLegalException("Index 不合法!" + index);
        }
    }


    //删除第一次出现关键字key的节点
    public void remove(int key) {
        //使用cur寻找关键字所在的节点
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == key) {
                if (cur == head) {//关键字在头节点
                    head = head.next;
                    //判断链表是否只有一个节点!
                    if(head!=null){
                        head.pre = null;
                    }else{
                        last=null;
                    }
                } else { //关键字在尾节点
                    if (cur == last) {
                        cur.pre.next = cur.next;
                        last = last.pre;
                    } else {  //关键字在中间节点
                        cur.pre.next = cur.next;
                        cur.next.pre = cur.pre;
                    }
                }

                return;
            }
            cur = cur.next;

        }

    }

    //删除所有值为key的节点
    public void removeAllKey(int key){
        //使用cur寻找关键字所在的节点
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == key) {
                if (cur == head) {//关键字在头节点
                    head = head.next;
                    //判断链表是否只有一个节点!
                    if(head!=null){
                        head.pre = null;
                    }else{
                        last=null;
                    }
                } else { //关键字在尾节点
                    if (cur == last) {
                        cur.pre.next = cur.next;
                        last = last.pre;
                    } else {  //关键字在中间节点
                        cur.pre.next = cur.next;
                        cur.next.pre = cur.pre;
                    }
                }

                //return;注释该语句,使其多次删除关键字为key的节点
            }
            cur = cur.next;

        }

    }


    //删除列表
    public void clear(){
        ListNode cur=head;
        while(cur!=null){
            ListNode curN=cur.next;
            cur.pre=null;
            cur.next=null;
            cur=curN;
        }
        head=last=null;
    }
}

 



详细讲解: 

 首先创建一个class文件:MyLinkedList类和一个Test类,前者用来实现双向链表,后者用来使用链表!

  在这个MyLinkedList类中,我们需要定义一个内部类:ListNode类,表示节点类!这个节点类应该包含val、pre、next三个成员变量和val的构造方法!

创建好内部类后,就可以定义MyLinkedList类中的成员变量!它应该包括头节点head和尾节点last!



1、一些简单的方法: 

通过前面单向链表的学习,一些简单的方法就不再过多详细介绍,大家看着代码就能懂其中的意思。

  //返回链表长度的方法:
    public int size(){
        int count =0;
        ListNode cur=head;
        while(cur!=null){
            cur=cur.next;
            count++;
        }
        return count;
    }


    //打印链表的方法;
    public void display(){
        ListNode cur=head;
        while(cur!=null){
            System.out.print(cur.val+" ");
            cur=cur.next;
        }
        System.out.println();
    }


    //查看是否包含key关键字的方法:
    public boolean contains(int key){
        ListNode cur=head;
        while(cur!=null){
            if(cur.val==key){
                return true;
            }
            cur=cur.next;
        }
        return false;
    }


2、头部插入的方法: 

头部插入前,首先需要实例化应该ListNode类的节点! 

头部插入的时候,需要分为两种情况:head==null 或 head!=null

i>当head==null时:

此时链表没有节点,此时head和last应该指向同一个节点node

ii>当head!=null时:

第一步:将head.next的null修改为新节点的地址0x78

第二步:将node.next修改为head的地址0x12

第三步:  修改头节点,head=node

修改前:

修改后: 

代码实现:


    //头部插入的方法
    public void addFirst(int data){
        ListNode node=new ListNode(data);
        //如果head为null
        if(head==null){
            head=last=node;
        }else{
            head.pre=node;
            node.next=head;
            head=node;
        }

    }



3、尾部插入的方法:

尾部插入的方法与头部插入的方法逻辑上是一样的,也分为两种情况:head==null 或 head!=null


    //尾部插入的方法:
    public void addLast(int data){
        ListNode node=new ListNode(data);
        //如果head==null
        if(head==null){
            head=last=node;
        }else{
            last.next=node;
            node.pre=last;
            last=node;
        }
    }


4、指定位置插入的方法:

指定位置插入方法全部代码:

   //指定位置插入的方法:
    public void addIndex(int index, int data) {
        //1、判断index是否合法
        try {
            checkIndex(index);//调用了checkIndex方法,方法实现在下面
        } catch (IndexNotLegalException e) {
            e.printStackTrace();
        }

        //index==0,相当于头部插入
        if (index == 0) {
            addFirst(data);
            return;
        }
        //index==size(),相当于尾部插入
        if(index==size()){
            addLast(data);
            return;
        }
        //0<index<size()
        if(index>0&&index<size()){
            //找到下标为index的节点
            ListNode cur=findIndex(index);//调用了findIndex方法,方法实现在下面
            //连接节点
            ListNode node=new ListNode(data);
            node.next=cur;
            node.pre=cur.pre;
            cur.pre.next=node;
            cur.pre=node;
            return;

        }
    }


//调用方法的实现:

    //找到index节点的方法:
    public ListNode findIndex(int index){
        int count=index;
        ListNode cur=head;
        while(count!=0){
            cur=cur.next;
            count--;
        }
        return  cur;
    }


    //检查index是否合法的方法
    private void checkIndex(int index){
        if(index<0||index>size()){
            throw new IndexNotLegalException("Index 不合法!"+index);
        }
    }

详细介绍; 

指定插入的方法需要传入一个下标,在指定下标的节点之前插入一个节点!

那么,根据下标的值可以分为四种情况:
i>下标不合法

此时先自定义一个异常类:

另外,需要在MyLinkedList类中创建一个方法,用来判断下标是否合法,如果不合法,抛出该异常类

//检查index是否合法的方法
    private void checkIndex(int index){
        if(index<0||index>size()){
            throw new IndexNotLegalException("Index 不合法!"+index);
        }
    }

 此时,就可以在指定位置插入的方法中写下标不合法的代码:

ii>index==0

当index==0,相当于头插,此时调用头部插入的方法即可

iii>index==size()

当index==size(),相当于尾部插入,此时调用尾部插入的方法即可

iiii>index>0&&index<size() 

这种情况属于指定位置插入的正常情况,它既不是头部插入,也不是尾部插入,而是在两个节点中间插入!

首先,需要使用创建cur找到下标为index的节点,以图为例子:我们要在下标为2的节点前插入node新节点!

那么,实例化node之后,我们就得根据如图中的箭头将新节点连接到链表中。可以看到,要修改四个引用的内容!

node.pre=cur.pre;

node.next=cur;

cur.pre.next=node;

cur.pre=node;

修改后:

代码实现:

//0<index<size()
        if(index>0&&index<size()){
            //找到下标为index的节点
            ListNode cur=findIndex(index);//调用findIndex方法
            //连接节点
            ListNode node=new ListNode(data);
            node.next=cur;
            node.pre=cur.pre;
            cur.pre.next=node;
            cur.pre=node;
            return;

        }

 调用的findIndex方法:

也是写在MyLinkedList类内部:

 //找到index节点的方法:
    public ListNode findIndex(int index){
        int count=index;
        ListNode cur=head;
        while(count!=0){
            cur=cur.next;
            count--;
        }
        return  cur;
    }


5、删除第一次出现关键字key的节点的方法:

删除第一次出现关键字key的节点,首先,先实例化一个cur帮助我们找到想要删除的节点

然后再执行删除操作,cur所在节点的位置不同,所要执行的操作也不同,这里分为三种情况:

1、cur所在节点为中间节点

2、cur==head

3、cur==last

先来说说第一种情况:cur所在节点为中间节点

首先,我们使用cur找到了关键字为12所在的节点!然后,执行删除操作!

这里只需要将cur所在的前后节点依照如图箭头方向连接即可!

cur.pre.next=cur.next;

cur.next.pre=cur.pre;

第二种情况:cur==head

这种情况下,我们会发现,如果照搬第一种情况的代码

cur.pre.next=cur.next;//由于head.pre==null,因此会报错

cur.next.pre=cur.pre;

所以,此时,我们只需要将这么写

head=head.next;  //头节点换到下一个节点

head.pre=null;     //将新的头节点的pre修改为null

特殊情况:

如果链表中只有一个节点!

那么执行完语句head=head.next后,head==null,因此语句head.pre=null(相当于null.pre=null)会报错!

所以,在cur==head的情况下,我们还要解决链表只有一个节点的特殊情况:

if (cur == head) {//关键字在头节点
                    head = head.next;
                    //判断链表是否只有一个节点!
                    if(head!=null){
                        head.pre = null;
                    }else{//只有一个节点的情况:
                        last=null;
                    }
                    
                }

第三种情况:cur==last

此时,这种情况下,代码这么写:

cur.pre.next=cur.next;  //将前一个节点的next置为null(cur.next==null)

last=last.pre;                //last向前移动一个节点

代码实现:


    //删除第一次出现关键字key的节点
    public void remove(int key) {
        //使用cur寻找关键字所在的节点
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == key) {
                if (cur == head) {//关键字在头节点
                    head = head.next;
                    //判断链表是否只有一个节点!
                    if(head!=null){
                        head.pre = null;
                    }else{
                        last=null;
                    }
                } else { //关键字在尾节点
                    if (cur == last) {
                        cur.pre.next = cur.next;
                        last = last.pre;
                    } else {  //关键字在中间节点
                        cur.pre.next = cur.next;
                        cur.next.pre = cur.pre;
                    }
                }

                return;//删完一个就走
            }
            cur = cur.next;

        }

    }


6、删除所有值为key的节点的方法:

有了上一个方法的学习,这个方法那就很简单了,只需要注释掉return语句即可,我们可以回头看看上述代码,它的整体逻辑是删除第一个关键字为key的节点就结束循环,那么,我们是不是就可以在删除完一个节点后选择不结束该方法,让它继续删除呢。当然可以!


    //删除所有值为key的节点
    public void removeAllKey(int key){
        //使用cur寻找关键字所在的节点
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == key) {
                if (cur == head) {//关键字在头节点
                    head = head.next;
                    //判断链表是否只有一个节点!
                    if(head!=null){
                        head.pre = null;
                    }else{
                        last=null;
                    }
                } else { //关键字在尾节点
                    if (cur == last) {
                        cur.pre.next = cur.next;
                        last = last.pre;
                    } else {  //关键字在中间节点
                        cur.pre.next = cur.next;
                        cur.next.pre = cur.pre;
                    }
                }

                //return;注释该语句,使其多次删除关键字为key的节点
            }
            cur = cur.next;

        }

    }


7、清空链表方法:

这里清空链表的主要逻辑是将每一个节点的pre和next置为null,最后将head和last置为null 


    //删除列表
    public void clear(){
        ListNode cur=head;
        while(cur!=null){
            ListNode curN=cur.next;
            cur.pre=null;
            cur.next=null;
            cur=curN;
        }
        head=last=null;
    }

 



三、LinkedList的使用

  上面我们讲解了如何实现双向链表,这其实是Java自带的LinkedList的底层实现,接下来让我们来学习Java自带的LinkedList吧!

1、LinkedList的构造

LinkedList有两个构造方法,在使用LinkedList之前,我们需要调用构造方法实例化一个对象。

方法:                                                解释:
LinkedList()                                       无参构造
public LinkedList(Collection<? extends E> c)         使用其他集合容器中元素构造List

第一个无参构造就不多解释了,因为比较好懂,那么我们来解释一下第二个构造方法可以传入那些参数?

首先,我们需要知道的是:

1、Collection是传入参数的类型

2、?表示:Collection<>中传入的类型

3、<? extends E>表示:?代表的这个类型要么继承E这个类型,要么继承E这个类型的子类

可以看到,第二个构造方法可以传入参数list,此时可能有以下疑问:

1、传入的参数类型是Collection类型的,那么为什么可以传入LinkedList类型的list呢?

答:LinkedList类型实现了Collection接口!

2、如何解释list符合<? extends E>

答:在实例化list的时候,LinkedList传入的参数类型是Integer,此时这个Integer代表  ?

      在实例化list2的时候,LinkedList传入的参数类型是Integer,此时这个Integr代表   E

      也即是说:? 继承了  E  这个类型,所以这个传入参数list是符合<? extends E>的

 

另外在实例化LinkedList的时候,因为LinkedList实现了List接口,因此在实例化的时候有两种写法:



 

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

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

相关文章

Codigger Desktop:用户体验与获得收益双赢的革新之作(一)

上周&#xff0c;我们介绍了Codigger Desktop凭借其强大的功能、稳定的性能以及人性化的设计&#xff0c;成为了广大开发者的得力助手。Codigger Desktop除了是开发者的利器外&#xff0c;它以其出色的用户体验和创新的收益模式&#xff0c;为用户提供了一个全新的选择。Codigg…

leetcode代码记录(下一个更大元素 II

目录 1. 题目&#xff1a;2. 我的代码&#xff1a;小结&#xff1a; 1. 题目&#xff1a; 给定一个循环数组 nums &#xff08; nums[nums.length - 1] 的下一个元素是 nums[0] &#xff09;&#xff0c;返回 nums 中每个元素的 下一个更大元素 。 数字 x 的 下一个更大的元素…

微信小程序真机无法下载文件

问题&#xff1a; 1、真机无法展示加了防盗链的图片 2、真机无法下载pdf等文件 文件服务器供应商&#xff1a;腾讯 解决&#xff1a; 1、在文件服务器控制台加上微信小程序的域名白名单&#xff1a;servicewechat.com 具体可查看&#xff1a;对象存储 设置防盗链-控制台指…

mysql结构与sql执行流程

Mysql的大体结构 客户端&#xff1a;用于链接mysql的软件 连接池&#xff1a; sql接口&#xff1a; 查询解析器&#xff1a; MySQL连接层 连接层&#xff1a; 应用程序通过接口&#xff08;如odbc,jdbc&#xff09;来连接mysql&#xff0c;最先连接处理的是连接层。 连接层…

STM32CubeMX+MDK通过I2S接口进行音频输入输出(全双工读写一个DMA回调)

一、前言 目前有一个关于通过STM32F411CEUx的I2S总线接口控制SSS1700芯片进行音频输入输出的研究。 SSS1700 是具有片上振荡器的 3S 高度集成的USB音频控制器芯片 。 SSS1700 功能支持96 KHz 24 位采样率&#xff0c;带外部音频编解码器&#xff08;24 位/96KHz I2S 输入和输出…

Java常用API_正则表达式_检验字符串是否满足规则——基础使用方法及综合练习

正则表达式可以校验字符串是否满足一定的规则&#xff0c;并用来校验数据格式的合法性。 简单举例&#xff1a; 校验一个qq号是否符合要求 要求&#xff1a;6位到20位之内&#xff0c;不能以0开头&#xff0c;必须全是数字 代码演示&#xff1a; public class Test1 {public…

【CicadaPlayer】视频切换/音视频同时切换

G:\CDN\all_players\CicadaPlayer-github-0.44\mediaPlayer\SuperMediaPlayer.hCicadaPlayer https://github.com/alibaba/CicadaPlayer可以clone 整个仓库的历史 git clone --bare https://github.com/username/project.git整体架构 :根据这个更容易理解:切换就是judgeFunc…

ElasticSearch的相关概念

文章目录 1、整体介绍1.1、elasticsearch的作用1.2、ELK技术栈1.3、elasticsearch和lucene1.4、为什么不是其他搜索技术&#xff1f;1.5、总结 2、倒排索引2.1、正向索引2.2、倒排索引2.3、总结 3、ES的一些概念3.1、文档和字段3.2、索引和映射3.3、mysql与elasticsearch3.4、安…

Mac安装配置Appium

一、安装 nodejs 与 npm 安装方式与 windows 类似 &#xff0c;官网下载对应的 mac 版本的安装包&#xff0c;双击即可安装&#xff0c;无须配置环境变量。官方下载地址&#xff1a;https://nodejs.org/en/download/ 二、安装 appium Appium 分为两个版本&#xff0c;一个是…

4.7总结(内部类,JDBC API || 离散化,树状数组)

JAVA学习小结 一.内部类 基础概念&#xff0c;用途和访问特点 什么是内部类&#xff1a;写在一个类中的另一个类称之为内部类&#xff1b; 内部类的用途&#xff1a;用于封装那些单独存在时没有意义&#xff0c;且是外部类的一部分的类&#xff08;汽车发动机&#xff0c;人…

flutter升级3.10.6Xcode构建报错

flutter sdk 升级Xcode报错收集&#xff0c;错误信息如下&#xff1a; Error (Xcode): Cycle inside Runner; building could produce unreliable results.没问题版本信息&#xff1a; Xcode&#xff1a;15.3 flutter sdk &#xff1a;3.7.12 dart sdk&#xff1a;2.19.6 …

【AI】ubuntu 22.04 本地搭建Qwen-VL 支持图片识别的大语言模型 AI视觉

下载源代码 yeqiangyeqiang-MS-7B23:~/Downloads/src$ git clone https://gh-proxy.com/https://github.com/QwenLM/Qwen-VL 正克隆到 Qwen-VL... remote: Enumerating objects: 584, done. remote: Counting objects: 100% (305/305), done. remote: Compressing objects: 10…

深度学习pytorch——RNN从表示到原理、应用、发展、实战详细讲解(终结)

时间序列的表示 题外话&#xff1a;在自然界中&#xff0c;我们接触到的信号都是模拟信号&#xff0c;在手机、电脑等电子设备中存储的信号是数字信号。我们通常对模拟信号进行等间隔取样得到数字信号&#xff0c;这个数字信号也被称为序列。将模拟信号转换为数据信号的过程称为…

C# wpf 嵌入外部程序

WPF Hwnd窗口互操作系列 第一章 嵌入Hwnd窗口 第二章 嵌入WinForm控件 第三章 嵌入WPF控件 第四章 嵌入外部程序&#xff08;本章&#xff09; 第五章 底部嵌入HwndHost 文章目录 WPF Hwnd窗口互操作系列前言一、如何实现&#xff1f;1、定义属性2、进程嵌入&#xff08;1&…

Windows11配置VUE开发环境

目录 一、按照nodejs二、命令安装npm cache clean --forcenpm install -g vue/clinpm install npm -gnpm install webpacknpm install vue-cli -g与npm install -g vue/cli区别npm install -g cnpm --registryhttps://registry.npm.taobao.orgnpm i yarn -g --verbosenpm i -g …

全面解析十七种数据分析方法,具象数据分析思维

一、介绍 在当今数据驱动的商业环境中&#xff0c;数据分析已经成为了企业获取竞争优势的关键工具。无论是为了优化运营效率&#xff0c;提高客户满意度&#xff0c;还是推动产品创新&#xff0c;企业都需要通过分析大量数据来做出明智的决策。数据分析方法多种多样&#xff0c…

计算机网络-文件传输及IP协议——沐雨先生

实验内容 编写请求文件的客户Java应用程序编写响应文件请求的服务器Java应用程序利用Wireshark查看和分析IP包 基本要求 使用Java语言建立请求文件的客户应用程序使用Java语言建立响应文件请求的服务器应用程序了解IP协议的工作过程了解IP包首部各字段及含义 对Java应用程序…

机器学习——模型融合:平均法

机器学习——模型融合&#xff1a;平均法 在机器学习领域&#xff0c;模型融合是一种通过结合多个基本模型的预测结果来提高整体模型性能的技术。模型融合技术通常能够降低预测的方差&#xff0c;提高模型的鲁棒性&#xff0c;并在一定程度上提高预测的准确性。本文将重点介绍…

数据结构初阶:栈和队列

栈 栈的概念及结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。 进行数据插入和删除操作的一端 称为栈顶&#xff0c;另一端称为栈底。 栈中的数据元素遵守后进先出 LIFO &#xff08; Last In First Out &#xff09;的原则。…

腾讯云4核8G服务器12M带宽646元1年零3个月,4C8G使用场景说明

腾讯云4核8G服务器多少钱&#xff1f;腾讯云4核8G轻量应用服务器12M带宽租用价格646元15个月&#xff0c;活动页面 txybk.com/go/txy 活动链接打开如下图所示&#xff1a; 腾讯云4核8G服务器优惠价格 这台4核8G服务器是轻量应用服务器&#xff0c;详细配置为&#xff1a;轻量4核…