JAVA-链表

1.链表的概念及结构

链表是一种物理存储结构上非连续存储结构(逻辑上连续),数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。

注意:

  1. 根据上图可看出,链表是在逻辑结构连续的,但是在物理结构上不一定
  2. 现实中的结点一般都是通过堆上申请出来的 
  3. 从堆上申请的空间 ,是按照一定的策略来分配的,两次申请的空间可能是连续的,也可能不连续

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

1. 单向或者双向 

2. 带头或者不带头 

 3. 循环或者非循环

  • 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如 哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。 
  • 无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。

2.无头单向非循环链表的实现 

2.1 创建类

public class MySingleLinkedList {static class ListNode{public int val;public ListNode next;public ListNode(int val) {//创建链表时赋值this.val = val;}}public ListNode head;//代表链表的头节点//创建链表public void createNode() {//默认存在这么多链表ListNode node1 = new ListNode(12);ListNode node2 = new ListNode(34);ListNode node3 = new ListNode(45);ListNode node4 = new ListNode(56);ListNode node5 = new ListNode(78);//创建好后,将他们连接起来node1.next = node2;node2.next = node3;node3.next = node4;node4.next = node5;//将表头,赋值给head标记this.head = node1;}
}

那么此时链表大概是这样的,地址是我随意编的:

2.2 addLast

尾插

//尾插public void addLast(int val) {//如果还没有创建节点,就把node做为首节点ListNode node = new ListNode(val);if(head == null) {head = node;return;}//遍历到末尾并连接//因为不能移动头结点,让一个引用指向头结点ListNode cur = head;//cur.next刚好走到最后一个结点while(cur.next != null) {cur = cur.next;}//循环出来刚好是最后一个结点,直接连接cur.next = node;}

注意:

循环遍历的时候一定注意循环判断条件的区别

  • 如果是cur != null

cur会遍历到这里才结束

  • 如果是cur.next != null

刚好遍历到最好一个结点,这样就可以直接和新结点连接

2.3 addFirst

头插

//头插public void addFirst(int val) {ListNode node = new ListNode(val);//1.链表是否为空if(head == null) {head = node;return;}//2.正常情况node.next = head;head = node;}

如果不是第一个结点,直接将新结点的next指向头结点,就连接起来了,再把新结点做为新的头结点

2.4 size

计算结点个数

//计算链表大小public int size() {int count = 0;ListNode cur = head;while(cur != null) {count++;cur = cur.next;}return count;}

唯一注意的依然是循环判断条件:必须是cur != null , 否则最后一个结点不会被统计到

2.5 addIndex

指定位置插入

//指定位置插入public void addIndex(int index, int val) {//1.判断index的合法性try {checkIndex(index);} catch (IndexNotLegalException e) {e.printStackTrace();}//2.index == 0 || index == size()if(index == 0) {//头插addFirst(val);return;}if(index == this.size()) {//尾插addLast(val);return;}//3.找到index前一个位置ListNode cur = findIndexSubOne(index);//4.链接//顺序不能改,不然就找不到后驱结点了ListNode node = new ListNode(val);node.next = cur.next;cur.next = node;}//找到要插入位置的前一个结点private ListNode findIndexSubOne(int index) {int count = 0;//写0就是把首地址默认为0下标,写1计算默认1下标ListNode cur = head;while (count != index-1) {cur = cur.next;count++;}return cur;}private void checkIndex(int index) {if(index < 0 || index > this.size()) {throw new IndexNotLegalException("index不合法");}}

涉及到下标越界访问最好设计一个异常:

public class IndexNotLegalException extends RuntimeException{public IndexNotLegalException() {super();}public IndexNotLegalException(String e) {super(e);}
}

为了更好的让你们理解画一图:

假设我要插入的位置是3

插入后:

2.5 contains 

是否有值为val的结点

public boolean contains(int val) {ListNode cur = head;while(cur != null) {//依次遍历,找到了就返回trueif(cur.val == val) {return true;}cur = cur.next;}return false;
}

2.6 remove

删除val值为key的第一个结点

public void remove(int key) {//1.null链表情况if(head == null) {return;}//2.删除首if(head.val == key) {head = head.next;return;}//3.正常情况ListNode cur = head;while(cur.next != null) {if(cur.next.val == key) {ListNode cN = cur.next;cur.next = cN.next;return;}cur = cur.next;}
}

正常情况可能难以理解,画个图解释一下:

假设我们要找到的是56

我估计会有人看迷糊循环条件,不是说cur.next不会算上最后一个结点吗?那最后一个结点怎么删除啊?

注意看我们的判断条件,我们判断的是cur.next.val == key 也就是下一个结点的值。如果刚好是最好一个结点的值,想象一下,cur会直接指向null。所以最后一个节点不会有问题,但是第一个节点是判断不到的。所以我们在前面就判断是否为第一个节点。

2.7 removeAllKey

删除所有key

public void removeAllKey(int key) {//正常处理ListNode prev = head;ListNode cur = head.next;while(cur != null) {if(cur.val == key){prev.next = cur.next;}else {prev = cur;}cur = cur.next;}//处理头节点,什么的程序没有探测头节点if(head.val == key) {head = head.next;}}

解析: 

 2.8 clear

关闭链表

直接让head为空,那就没有引用指向链表了他们都会被JVM回收

public void clear() {this.head = null;
}

2.9 display

这并不是链表里面有的一个方法,为了方便看结构,写的输出方法

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

我就不带着大家看最终结构了,自己复制过去看看吧

3.什么是LinkedList

LinkedList的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的节 点中,然后通过引用将节点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。

在集合框架中,LinkedList也实现了List接口,具体如下:

【说明】

1. LinkedList实现了List接口

2. LinkedList的底层使用了双向链表 

3. LinkedList没有实现RandomAccess接口

4. LinkedList的任意位置插入和删除元素时效率比较高,删除的时间复杂度为O(n)不需要移动元素,插入的复杂度还是O(n)但依然因为不需要移动元素比顺序表快

5. LinkedList比较适合任意位置插入的场景

3.1 LinkedList实现

大部分代码都很简单我就不啰嗦了,有了前面的基础这个很简单。

// 2、无头双向链表实现
public class MyLinkedLIst {static class ListNode {int val;public ListNode prev;//前驱public ListNode next;//后驱public ListNode(int val){this.val = val;}}public ListNode head;//头节点public ListNode last;//尾节点//得到单链表的长度public int size(){ListNode cur = head;int count = 0;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);if(head == null) {//1.还没有节点head = last = node;} else {//2.正常情况node.next = head;head.prev = node;head = node;}}//尾插法public void addLast(int data){ListNode node = new ListNode(data);if(last == null) {//1.没有节点情况head = last = node;}else {last.next = node;node.prev = last;last = node;}}//任意位置插入,第一个数据节点为0号下标public void addIndex(int index,int data) {//1.index是否合法try {checkIndex(index);}catch (IndexNotLegalException e) {e.printStackTrace();}//2.是否是头插或尾插if(index == 0) {addFirst(data);return;}if (index == size()) {addLast(data);return;}//3.正常情况ListNode node = new ListNode(data);//找到目标位置的前面位置ListNode tmp = findIndex(index);node.next = tmp.next;tmp.next = node;node.prev = tmp;node.next.prev = node;}private void checkIndex(int index) {if(index < 0 || index > size()) {throw new IndexNotLegalException("双向链表插入位置不合法: " + index);}}private ListNode findIndex(int index) {ListNode cur = head;while (index != 0) {cur = cur.next;index--;}return cur;}//删除第一次出现关键字为key的节点public void remove(int key) {ListNode cur = head;while (cur != null) {if(cur.val == key) {if(cur == head) {head = head.next;if(head.next == null) {//说明现在链表中只有一个节点last = null;}else {head.prev = null;}}else {//正常情况下cur.prev.next = cur.next;if(cur.next == null) {//已经是最后一个节点last = last.prev;}else {cur.next.prev = cur.prev;}}return;//删完一个就走}cur = cur.next;}}//删除所有值为key的节点public void removeAllKey(int key){ListNode cur = head;while (cur != null) {if(cur.val == key) {if(cur == head) {head = head.next;if(head.next == null) {//说明现在链表中只有一个节点last = null;}else {head.prev = null;}}else {//正常情况下cur.prev.next = cur.next;if(cur.next == null) {//已经是最后一个节点last = last.prev;}else {cur.next.prev = cur.prev;}}//return;//删完一个就走}cur = cur.next;}}public void clear(){
//        last = head = null;  暴力版ListNode cur = head;while(cur != null) {ListNode curN = cur.next;cur.prev = null;cur.next = null;cur = curN;}}
}

3.2 remove和removeAllKey详解

唯一可能看不懂的就是remove,有太多判断条件了,那我就给大家一步步解释一下:

public void remove2(int key) {ListNode cur = head;while (cur != null) {if(cur.val == key) {//把前驱结点的后驱,指向cur的后驱cur.prev.next = cur.next;//把后驱结点的前驱,指向cur的前驱cur.next.prev = cur.prev;return;}cur = cur.next;}
}

如果没有一些特殊情况这也就删完了。但是还有如果是最后一个结点,或者第一个结点就不能这么写。

处理删除最后一个结点情况: 

public void remove(int key) {ListNode cur = head;while (cur != null) {if(cur.val == key) {//把前驱结点的后驱,指向cur的后驱cur.prev.next = cur.next;//如果删除最后一个结点if(cur.next == null) {//让last直接往后走一步,就没有引用指向他了last = last.prev;}else {//把后驱结点的前驱,指向cur的前驱cur.next.prev = cur.prev;}return;}cur = cur.next;}
}

处理删除头结点的情况:

public void remove2(int key) {ListNode cur = head;while (cur != null) {if(cur.val == key) {//删除头的情况if (cur == head) {//直接让head向后走一步head = head.next;//再将head前驱置为nullhead.prev = null;}else {//把前驱结点的后驱,指向cur的后驱cur.prev.next = cur.next;//如果删除最后一个结点if(cur.next == null) {//让last直接往后走一步,就没有引用指向他了last = last.prev;}else {//把后驱结点的前驱,指向cur的前驱cur.next.prev = cur.prev;}}return;}cur = cur.next;}
}

肯定很多人觉得差不多了,特殊情况都处理完了。但其实如果只有一个结点这里会报错

处理一个结点情况:

public void remove2(int key) {ListNode cur = head;while (cur != null) {if(cur.val == key) {//删除头的情况if (cur == head) {//直接让head向后走一步head = head.next;//处理只有一个结点的情况if (head == last) {head = last = null;}else {//再将head前驱置为nullhead.prev = null;}}else {//把前驱结点的后驱,指向cur的后驱cur.prev.next = cur.next;//如果删除最后一个结点if(cur.next == null) {//让last直接往后走一步,就没有引用指向他了last = last.prev;}else {//把后驱结点的前驱,指向cur的前驱cur.next.prev = cur.prev;}}return;//删完一个就走}cur = cur.next;}
}

 那如果是removeAllKey,就是删除所有key。直接把return屏蔽就可以了,他会遍历完所有的结点。

3.2 LinkedList的使用

3.2.1 LinkedList的构造

public static void main(String[] args) {LinkedList<Integer> list1 = new LinkedList<>();list1.add(1);list1.add(2);LinkedList<Integer> list2 = new LinkedList<>(list1);System.out.println(list2);//1 , 2ArrayList<Integer> list3 = new ArrayList<>();list3.add(12);list3.add(34);LinkedList<Integer> list4 = new LinkedList<>(list3);System.out.println(list4);//12 , 34
}

3.2.2 常用方法介绍

public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();//默认为尾插list.add(1);//1list.add(2);//1 2list.add(3);//1 2 3//指定位置插入list.add(2,10);//1 2 10 3ArrayList<Integer> arrayList = new ArrayList<>();arrayList.add(12);//12arrayList.add(34);//12 34//尾插所有元素list.addAll(arrayList);//1 2 10 3 12 34//删除下标为1的list.remove(1);// 1 10 3 12 34//删除值为3的结点//需要装箱,才好和int类型的remove做区分list.remove(Integer.valueOf(3));//1 10 12 34}

 

public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(12);//12list.add(23);//12 23list.add(34);//12 23 34list.add(45);//12 23 34 45list.add(56);//12 23 34 45 56//访问下标为2的元素System.out.println(list.get(2));//34//将下标为3的元素改为10list.set(3,10);//12 23 34 10 56//查找链表中是否有56System.out.println(list.contains(56));//true//返回元素为23的下标System.out.println(list.indexOf(23));//1list.add(12);//12 23 34 10 56 12//倒叙往前遍历,返回第一个访问到12的下标System.out.println(list.lastIndexOf(12));//5}

 

3.2.3 subList

截取代码中的指定区域

public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(12);//12list.add(23);//12 23list.add(34);//12 23 34list.add(45);//12 23 34 45list.add(56);//12 23 34 45 56List<Integer> list1 = list.subList(1,4);System.out.println(list1);//23 34 45list1.set(0,100);//100 34 45System.out.println(list);//12 100 34 45 56
}

他返回的是指定区域的首地址,所以对list1修改也会影响到list

4. LinkedList打印遍历

public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(1);list.add(2);list.add(3);list.add(4);list.add(5);System.out.println(list);System.out.println("===foreach===");for(Integer e : list) {System.out.print(e + " ");}System.out.println();System.out.println("===for===");for (int i = 0; i < list.size(); i++) {System.out.print(list.get(i) + " ");}System.out.println();System.out.println("===Iterator===");//返回的是一个新的结点指向链表头,数组中-1位置Iterator<Integer> it1 = list.iterator();//判断下一个是否有结点while (it1.hasNext()) {//移动到下一个结点并返回值System.out.print(it1.next() + " ");}System.out.println();System.out.println("===ListIterator===");//专门给List打印的ListIterator<Integer> it2 = list.listIterator();while (it2.hasNext()) {System.out.print(it2.next() + " ");}System.out.println();System.out.println("===listIterator(list.size())===");//倒叙打印ListIterator<Integer> it3 = list.listIterator(list.size());while (it3.hasPrevious()) {System.out.print(it3.previous() + " ");}}

结果:

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

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

相关文章

RTSP播放器EasyPlayer.js播放器UniApp或者内嵌其他App里面webview需要截图下载

EasyPlayer.js H5播放器&#xff0c;是一款能够同时支持HTTP、HTTP-FLV、HLS&#xff08;m3u8&#xff09;、WS、WEBRTC、FMP4视频直播与视频点播等多种协议&#xff0c;支持H.264、H.265、AAC、G711A、Mp3等多种音视频编码格式&#xff0c;支持MSE、WASM、WebCodec等多种解码方…

DB Type

P位 p 1时段描述符有效&#xff0c;p 0时段描述符无效 Base Base被分成了三个部分&#xff0c;按照实际拼接即可 G位 如果G 0 说明描述符中Limit的单位是字节&#xff0c;如果是G 1 &#xff0c;那么limit的描述的单位是页也就是4kb S位 S 1 表示代码段或者数据段描…

【Fargo】23:采集时间转rtp时间

RTP时间戳 编码会沿用当前时间,以毫秒计算,而rtp传输系统采用的是时间基准并不是当前时间RTP 时间戳为了多媒体不同流之间实现同步而设计的。Mediasoup的clockrate参数就是指定这个的 采集实现戳是当前时间uint32类型的毫秒,如果使用rtp发送h264编码的rtp包,时间戳要怎么打…

Android Osmdroid + 天地图 (一)

Osmdroid 天地图 前言正文一、配置build.gradle二、配置AndroidManifest.xml三、获取天地图的API Key① 获取开发版SHA1② 获取发布版SHA1 四、请求权限五、显示地图六、源码 前言 Osmdroid是一款完全开源的地图基本操作SDK&#xff0c;我们可以通过这个SDK去加一些地图API&am…

HTML5+CSS前端开发【保姆级教学】+新闻文章初体验

Hello&#xff0c;各位编程猿们&#xff01;上一篇文章介绍了前端以及软件的安装&#xff0c;这一篇我们要继续讲解页面更多知识点&#xff0c;教大家做一篇新闻题材的文章 新闻文章 当我们点开浏览器经常看到各种各样的文章&#xff0c;今天我们就来看看大家最喜欢关注的体育…

无人机动力系统测试-实测数据与CFD模拟仿真数据关联对比分析

我们经常被问到这样的问题&#xff1a;“我们计划运行 CFD 仿真&#xff0c;我们还需要对电机和螺旋桨进行实验测试吗&#xff1f;我们可能有偏见&#xff0c;但我们的答案始终是肯定的&#xff0c;而且有充分的理由。我们自己执行了大量的 CFD 仿真&#xff0c;但我们承认&…

【HarmonyOS】鸿蒙系统在租房项目中的项目实战(二)

从今天开始&#xff0c;博主将开设一门新的专栏用来讲解市面上比较热门的技术 “鸿蒙开发”&#xff0c;对于刚接触这项技术的小伙伴在学习鸿蒙开发之前&#xff0c;有必要先了解一下鸿蒙&#xff0c;从你的角度来讲&#xff0c;你认为什么是鸿蒙呢&#xff1f;它出现的意义又是…

深度学习神经网络创新点方向

一、引言 深度学习神经网络在过去几十年里取得了令人瞩目的成就&#xff0c;从图像识别、语音处理到自然语言理解等众多领域都有广泛应用。然而&#xff0c;随着数据量的不断增长和应用场景的日益复杂&#xff0c;对神经网络的创新需求也愈发迫切。本文将探讨深度学习神经网络…

C++析构函数详解

C析构函数详解&#xff1a;对象销毁与资源清理 在 C 中&#xff0c;析构函数是与构造函数相对应的特殊成员函数&#xff0c;它在对象生命周期结束时被自动调用&#xff0c;用于执行对象销毁之前的清理操作。析构函数主要用于释放对象占用的资源&#xff0c;如动态分配的内存、打…

Minikube 上安装 Argo Workflow

文章目录 步骤 1&#xff1a;启动 Minikube 集群步骤 2&#xff1a;安装Argo Workflow步骤 3&#xff1a;访问UI创建流水线任务参考 前提条件&#xff1a; Minikube&#xff1a;确保你已经安装并启动了 Minikube。 kubectl&#xff1a;确保你已经安装并配置了 kubectl&#xff…

计算机编程中的设计模式及其在简化复杂系统设计中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 计算机编程中的设计模式及其在简化复杂系统设计中的应用 计算机编程中的设计模式及其在简化复杂系统设计中的应用 计算机编程中的…

基于 CentOS7.6 的 Docker 下载常用的容器(MySQLRedisMongoDB),解决拉取容器镜像失败问题

安装MySQL&Redis&MongoDB mysql选择是8版本&#xff0c;redis是选择4版本、mongoDB选择最新版&#xff0c;也可以根据自己的需要进行下载对应的版本&#xff0c;无非就是容器名:版本号 这样去拉去相关的容器镜像。如果你还不会在服务器中安装 docker&#xff0c;可以查…

【分布式】万字图文解析——深入七大分布式事务解决方案

分布式事务 分布式事务是指跨多个独立服务或系统的事务管理&#xff0c;以确保这些服务中的数据变更要么全部成功&#xff0c;要么全部回滚&#xff0c;从而保证数据的一致性。在微服务架构和分布式系统中&#xff0c;由于业务逻辑往往会跨多个服务&#xff0c;传统的单体事务…

SystemVerilog学习笔记(十一):接口

在Verilog中&#xff0c;模块之间的通信是使用模块端口指定的。 Verilog模块连接的缺点 声明必须在多个模块中重复。存在声明不匹配的风险。设计规格的更改可能需要修改多个模块。 接口 SystemVerilog引入了 interface 结构&#xff0c;它封装了模块之间的通信。一个 inter…

ARM 汇编指令

blr指令的基本概念和用途 在 ARM64 汇编中&#xff0c;blr是 “Branch with Link to Register” 的缩写。它是一种分支指令&#xff0c;主要用于跳转到一个由寄存器指定的地址&#xff0c;并将返回地址保存到链接寄存器&#xff08;Link Register&#xff0c;LR&#xff09;中。…

pycharm分支提交操作

一、Pycharm拉取Git远程仓库代码 1、点击VCS > Get from Version Control 2、输入git的url&#xff0c;选择自己的项目路径 3、点击Clone&#xff0c;就拉取成功了 默认签出分支为main 选择develop签出即可进行开发工作 二、创建分支&#xff08;非必要可以不使用&#xf…

【MySQL】优化方向+表连接

目录 数据库表连接 表的关系与外键 数据库设计 规范化 反规范化 事务一致性 表优化 索引优化 表结构优化 查询优化 数据库表连接 表的关系与外键 表之间的关系 常见表关系总结 一对一关系&#xff1a;每一条记录在表A中对应表B的唯一一条记录&#xff0c;反之也是&a…

【数据库】mysql数据库迁移前应如何备份数据?

MySQL 数据库的备份是确保数据安全的重要措施之一。在进行数据库迁移之前&#xff0c;备份现有数据可以防止数据丢失或损坏。以下是一套详细的 MySQL 数据库备份步骤&#xff0c;适用于大多数情况。请注意&#xff0c;具体的命令和工具可能因 MySQL 版本的不同而有所差异。整个…

mybatis 动态SQL语句

10. 动态SQL 10.1. 介绍 什么是动态SQL&#xff1a;动态SQL指的是根据不同的查询条件 , 生成不同的Sql语句. 官网描述&#xff1a;MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验&#xff0c;你就能体会到根据不同条件拼接 SQL 语句的痛苦…

shell脚本_永久环境变量和字符串操作

一、永久环境变量 1. 常见的环境变量 2. 设置永久环境变量 3.1.将脚本加进PATH变量的目录中 3.2.添加进环境变量里 3.2.修改用户的 shell 配置文件 二、字符串操作 1. 字符串拼接 2. 字符串切片 3. 字符串查找 4. 字符串替换 5. 字符串大小写转换 6. 字符串分割 7…