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为nullif (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==nullif (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为nullif(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==nullif(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接口,因此在实例化的时候有两种写法:



 

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

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

相关文章

C++面向对象程序设计 - 共用数据的保护(常对象、常指针和常引用)

C虽然采取了不少有效的措施&#xff08;如设private保护&#xff09;以增加数据的安全性&#xff0c;但是有些数据却往往是共享的&#xff0c;例如实参与形参&#xff0c;变量与其引用&#xff0c;数据与其指针等&#xff0c;人们可以在不同的场合通过不同的途径访问同一个数据…

【QT+QGIS跨平台编译】056:【pdal_arbiter+Qt跨平台编译】(一套代码、一套框架,跨平台编译)

点击查看专栏目录 文章目录 一、pdal_arbiter介绍二、pdal下载三、文件分析四、pro文件五、编译实践一、pdal_arbiter介绍 pdal_arbiter是 PDAL 项目的一个库,用于帮助管理应用程序运行在 EC2 实例上的 AWS 凭证。 当应用程序需要调用 AWS API 时,它们必须使用 AWS 凭据对 AP…

AI预测福彩3D第28弹【2024年4月6日预测--第7套算法重新开始计算第1次测试】

今天开始&#xff0c;咱们开始进行第7套算法的测试&#xff0c;第7套算法将综合012路权重、012路直选及012路和值进行预测。好了&#xff0c;先上图后上结果吧~ 2024年4月6日福彩3D的七码预测结果如下 第一套&#xff1a; 百位&#xff1a;1 2 4 5 7 8…

全志 Linux Qt

一、简介 本文介绍基于 buildroot 文件系统的 QT 模块的使用方法&#xff1a; • 如何在 buildroot 工具里编译 QT 动态库&#xff1b; • 编译及运行 qt_demo 应用程序&#xff1b; • 适配过程遇到的问题。 二、QT动态库编译 在项目根路径执行 ./build.sh buildroot_menuc…

武汉星起航:打造亚马逊一站式孵化平台引领电商新风潮

2020年正式成立后&#xff0c;武汉星起航持续深耕亚马逊自营店铺运营&#xff0c;不断拓展跨境电商业务。公司凭借专业运营团队和多年经验为合作伙伴提供深入合作模式&#xff0c;迅速崭露头角。推出亚马逊一站式孵化平台&#xff0c;为卖家提供全方位支持&#xff0c;彰显了公…

[RAM] HBM 导论 | 为什么我们需要 HBM?

主页&#xff1a; 元存储博客 文章目录 前言1. 什么是 HBM2. HBM 发展2.1 HBM 起源2.2 HBM 简史 3. HBM 3D 结构原理4. 为什么需要 HBM5. HBM 生产商家6. HBM 封装厂家7. HBM 应用8. HBM 挑战8.1 工艺成本8.2 量产良率问题8.3 散热问题 总结 前言 在AI时代的浪潮中&#xff0c…

vue项目入门——index.html和App.vue

vue项目中的index.html文件 在Vue项目中&#xff0c;index.html文件通常作为项目的入口文件&#xff0c;它包含了Vue应用程序的基础结构和配置。 该文件的主要作用是引入Vue框架和其他必要的库&#xff0c;以及定义Vue应用程序的启动配置。 import Vue from vue import App …

Android Studio学习8——点击事件

在xml代码中绑定 在java代码中绑定 弹出一个toast 随机&#xff0c;数组

H5 点击图片翻转效果

需求 ☑ h5 实现点击图片得到的是放大的镜像图片&#xff08;不是放大镜效果 而是实现图片镜像对折&#xff0c;左右翻转&#xff09; ☑ 鼠标点击后原图消失/隐藏&#xff0c;在原来的位置上取而代之的是翻转后的图&#xff08;除了翻转之外不要改变其他的性质&#xff0c;比…

Langchain教程 | langchain+OpenAI+PostgreSQL(PGVector) 实现全链路教程,简单易懂入门

前提&#xff1a; 在阅读本文前&#xff0c;建议要有一定的langchain基础&#xff0c;以及langchain中document loader和text spliter有相关的认知&#xff0c;不然会比较难理解文本内容。 如果是没有任何基础的同学建议看下这个专栏&#xff1a;人工智能 | 大模型 | 实战与教程…

【CicadaPlayer】demuxer_service中DASH的简单理解

DASH协议 dash 是属于demuxer模块的 MPEG-DASH是一种自适应比特率流技术,可根据实时网络状况实现动态自适应下载。和HLS, HDS技术类似, 都是把视频分割成一小段一小段, 通过HTTP协议进行传输,客户端得到之后进行播放;不同的是MPEG-DASH支持MPEG-2 TS、MP4(最新的HLS也支持…

Linux 多线程

目录 初识线程 线程的概念 Linux下的线程 线程优缺点 线程控制 线程创建 线程终止 线程等待 线程分离 线程取消 其它 线程互斥 互斥的概念 互斥锁的使用 锁的本质 线程同步 线程同步的概念 条件变量的概念 条件变量的使用 信号量 信号量的概念 信号量接口…

非机构化解析【包含PDF、word、PPT】

此项目是针对PDF、docx、doc、PPT四种非结构化数据进行解析&#xff0c;识别里面的文本和图片。 代码结构 ├── Dockerfile ├── requirements ├── resluts ├── test_data │ ├── 20151202033304658.pdf │ ├── 2020_World_Energy_Data.pdf │ ├── …

【Web】纯萌新的BUUCTF刷题日记Day1

目录 [RoarCTF 2019]Easy Java [网鼎杯 2018]Fakebook [CISCN2019 华北赛区 Day2 Web1]Hack World [BJDCTF2020]The mystery of ip [网鼎杯 2020 朱雀组]phpweb [BSidesCF 2020]Had a bad day [BJDCTF2020]ZJCTF&#xff0c;不过如此 [BUUCTF 2018]Online Tool [GXYCTF…

虚拟主机VPS和共享服务器有什么区别?VPS和共享服务器怎么选择,VPS和云服务器区别

今天易极赞小编来跟大家科普一个新的知识“虚拟主机和云服务器有什么区别&#xff1f;”看完这篇文章后你应该就能知道虚拟主机和云服务器哪个更适合你了。 如果你不知道服务器的常见类型有哪些&#xff0c;查看下面这篇文章&#xff1a; 服务器7中常见的类型&#xff0c;服务…

【C语言】如何判断一个机器的大小端

如何判断一个机器的大小端 一&#xff1a;什么是机器的大小端二&#xff1a;为什么会有大小端三&#xff1a;设计一个小程序来判断当前机器的大小端方法一&#xff1a;指针类型强转方法二&#xff1a;联合体 一&#xff1a;什么是机器的大小端 机器的大小端是指在内存中存储多…

【移动安全】对webview漏洞的一些分析

这次分析的app如下&#xff1a; 打开发现该app发现需要登录界面&#xff1a; 拖进jadx看一下&#xff0c;先来看一下AndroidManifest.xml文件 发现有两个类是导出&#xff0c;再来分析这两个类 这个RegistrationWebView类利用webview.loadUrl进行加载网页 java public class…

JS——判断节假日(假日包括周末,不包括调休上班的周末)

思路&#xff1a;创建两个数组&#xff0c;数组1为节假日数组&#xff0c;数组2为是周末上班日期数组。如果当前日期&#xff08;或某日期&#xff09;同时满足2个条件&#xff08;1.在节假日数组内或在周末。2.不在周末上班日期数组&#xff09;即为节假日&#xff0c;否则即为…

SystemC入门学习Demo用例的工程化配置

背景&#xff1a;对不同的用例文件&#xff0c;使用CMakeLists.txt进行工程化管理的演示&#xff0c;这样开发者可以更加关注在代码开发上。 1&#xff0c;首先安装好系统环境的systemC库&#xff1a;ubuntu系统安装systemc-2.3.4流程-CSDN博客 2&#xff0c;准备好一个demo用…

再续前缘——C++【入门】

目录 1. 引用 引用概念 使用场景 1. 做参数 2. 引用做返回值 3.传值、传引用效率比较 4. 引用和指针的不同点 2. 内联函数 3.auto关键字 推导应用场景 auto不能推导的场景 4.基于范围的for循环(C11) 5.指针空值nullptr(C11) 1. 引用 引用概念 引用不是新定义一个…