JavaDS —— 单链表 与 LinkedList
顺序表和链表区别
ArrayList :
底层使用连续的空间,可以随机访问某下标的元素,时间复杂度为O(1)
但是在插入和删除操作的时候,需要将该位置的后序元素整体往前或者向后移动,时间复杂度为O(N)
增容需要申请新空间,有时候需要拷贝数据释放旧空间,这会有不小的消耗。
顺序表的增容一般是2倍增加的,势必会有一定的kong’jian浪费,例如当前容量为100时,需要扩容的话,就是将容量增加到200,如果只是再插入几个数据,就一定会浪费九十几的空间。
既然如此,我们就会思考如何减少空间的浪费,这时候链表就登场了,下面是单链表的示意图:
链表由两个部分组成,一个是数据域,一个是指针域,数据域是用来存放数据的,指针域是用来存放下一个或者前一个的引用的,这样就把数据给串联起来了,大家也就不难发现,链表的优点就是用多少空间就申请多少空间,做到空间不浪费,并且在下面的内容,你还会感受到链表的插入删除操作效率很高。
链表的分类
链表有8大类,带头和不带头,单向还是双向,循环还是不循环,2^3 = 8种
带头和不带头是指链表有没有一个哨兵节点,就是只是充当头结点的作用,不存放任何有效的数据。
上面的图片就是不带头的,下面的是带头的:
单向和双向是指:链表的节点是只指向后一个节点的话就是单向的,如果链表的节点即指向前一个结点又指向后一个节点的话就是双向的。
循环和不循环是指链表是否头尾相连,如果头尾相连就是循环的,否则就是不循环的:
实现单链表
下面是自己写的IList接口,会被单链表拓展:
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();
}
单链表的节点需要一个数据域和一个指针域,我们先来写一个静态内部类来构造节点类:
static class ListNode {public int val;public ListNode next;public ListNode(int val) {this.val = val;}}
除此之外,我们还需要一个头指针来指向第一个节点:
public ListNode head;
打印
循环遍历链表,打印每一个节点的数据,这个方法有利于我们的测试:
@Overridepublic void display() {ListNode cur = head;while(cur != null) {System.out.print(cur.val + " ");cur = cur.next;}System.out.println();}
头插
在单链表的头部插入一个数据,我们需要将新头节点的next指向原先头部的节点,然后改变head的指向。
@Overridepublic void addFirst(int data) {ListNode node = new ListNode(data);node.next = head;head = node;}
尾插
循环遍历单链表找到尾节点,然后改变尾节点的指向即可。
这里要注意如果head为空的时候,直接赋值就可以了,不能直接使用null,会报空指针异常,所以在循环前面加多一个判断条件即可。
@Overridepublic void addLast(int data) {ListNode node = new ListNode(data);if(head == null) {head = node;return;}ListNode cur = head;while(cur.next != null) {cur = cur.next;}cur.next = node;}
求节点总个数
这个很简单,直接循环遍历即可。
@Overridepublic int size() {ListNode cur = head;int count = 0;while(cur != null) {cur = cur.next;count++;}return count;}
指定位置插入
先判断指定的位置有没有越界,和之前的顺序表是一样的,这里不赘述:
public class IndexException extends RuntimeException{public IndexException(String message) {super(message);}
}
private void checkIndexInAdd(int index) throws IndexException {if(index < 0 || index > size()) {throw new IndexException("下标范围不合法!");}}
我们要先找到index前一个结点,因为这个插入操作是对三个节点进行操作的,首先先把index的引用放入新结节点的next中,然后再把index前一个结点的next改成新结点的引用,这是一般情况,如果index == 0的话就是头插操作,为什么要做一个判断,因为我们得出的一般规律最后是cur.next = node,这是建立在新结点前面一定有结点的情况下,但是如果是头插的话就不符合了,所以头插需要单独说明。
@Overridepublic void addIndex(int index, int data) {try{checkIndexInAdd(index);if(index == 0) {addFirst(data);return;}//找到index前一个的节点ListNode cur = head;for (int i = 0; i < index - 1; i++) {cur = cur.next;}ListNode node = new ListNode(data);node.next = cur.next;cur.next = node;} catch (IndexException e) {System.out.println("index 不合法!");e.printStackTrace();}}
对于插入操作,我们要先处理后面的结点,避免后面的结点丢失。
contains
是否包含某个元素,直接遍历循环即可:
@Overridepublic boolean contains(int key) {ListNode cur = head;while(cur != null) {if(cur.val == key) {return true;}cur = cur.next;}return false;}
删除第一次出现的key
删除某个结点的时候,由于这是单链表,所以我们最好事先拿到删除节点的前一个结点,然后我们要考虑一些特殊的情况,如果这个链表为空就不需要删除,如果要删除的结点就是头结点,那么我们就需要改变头指针的指向,最后就是一般情况下,我们直接修改删除结点的前一个结点的 next 域 就可以了。
private ListNode findFrontNodeOfKey(int key) {ListNode cur = head;while(cur != null) {if(cur.next.val == key) {return cur;}cur = cur.next;}return null;}@Overridepublic void remove(int key) {//空链表if(head == null) {return;}//头删if(head.val == key) {head = head.next;return;}ListNode prev = findFrontNodeOfKey(key);if(prev == null) {return;//不存在key}ListNode del = prev.next;prev.next = del.next;}
删除所有出现的key
我们使用两个指针,一个从头结点开始,另一个从头结点的下一个结点开始遍历链表,当第二个指针遇到要删除的结点时,配合第一个指针完成此工作,然后prev不变,cur继续移动,如果没有遇到删除的结点,两个指针是一起继续向后运动。
要注意如果链表为空的话就直接return ,避免发生空指针异常
这时候大家一定知道还差一个结点没有判断,就是第一个结点,所以我们最后还有判断一下头结点。
@Overridepublic void removeAllKey(int key) {if(head == null) {return;}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;}}
clear
清空链表,你可以直接把头指针赋值为null,由于链表没有被引用,会被JVM自动回收,
@Overridepublic void clear() {ListNode cur = head;while(cur != null) {ListNode tmp = cur.next;cur.next = null;cur = tmp;}head = null;}
模拟实现LinkedList
LinkedList 是不带头,双向的,循环的链表
构建节点
双向的意味着有两个节点,一个指向前一个结点,一个指向后一个结点,还有一个头指针指向头节点,一个尾指针指向尾节点。
static class ListNode {public int val;public ListNode prev;public ListNode next;public ListNode(int val) {this.val = val;}}public ListNode head;public ListNode last;
打印
public void display() {ListNode cur = head;while(cur != null) {System.out.print(cur.val + " ");cur = cur.next;}System.out.println();}
头插
要注意如果头指针为null,意味着链表为空,尾指针自然也是null,链表为空的话,插入新数据要改变头尾指针的指向。
正常情况下是链表至少有一个结点,改变原先头节点的prev指向,新结点的next也要改变。
public void addFirst(int data) {ListNode node = new ListNode(data);if(head == null) {head = last = node;return;}head.prev = node;node.next = head;head = node;}
尾插
注意如果尾指针为null时,说明链表为空。和上面的头插一样,要单独讨论说明。
public void addLast(int data) {ListNode node = new ListNode(data);if(last == null) {head = last = node;}last.next = node;node.prev = last;last = node;}
求结点个数
public int size() {ListNode cur = head;int count = 0;while(cur != null) {count++;cur = cur.next;}return count;}
指定位置插入
先判断index是否合法,不合法还是和之前一样抛异常。
public class IndexOutOfBoundException extends RuntimeException {public IndexOutOfBoundException() {super();}public IndexOutOfBoundException(String message) {super(message);}
}
private void checkIndexInAdd(int index) throws IndexOutOfBoundException{if(index < 0 || index > size()) {throw new IndexOutOfBoundException("下标越界!!!");}}
我们先讨论一般情况,如果待插入的结点正好前后都是由结点的,那么我们需要修改三个结点的指针:
cur.prev.next = node;
node.prev = cur.prev;
node.next = cur;
cur.prev = node;
现在来注意特殊情况,如果index == 0时,就是头插,不管怎么样,头插就一定要改变头指针,所以要单独讨论。换一种思路,如果是头插的话,cur.prev = null ,所以 cur.prev.next 一定会报空指针异常。所以头插还是要单独讨论。
那如果是尾插呢?尾插意味着 cur == null ,还是和头插思考方式一样,尾节点一定要改变所以要单独讨论,还有cur.prev 一定会报空指针异常。
public void addIndex(int index,int data) {try {checkIndexInAdd(index);if(index == 0) {addFirst(data);return;}ListNode node = new ListNode(data);ListNode cur = head;for (int i = 0; i < index; i++) {cur = cur.next;}if(cur == null) {addLast(data);return;}cur.prev.next = node;node.prev = cur.prev;node.next = cur;cur.prev = node;} catch (IndexOutOfBoundException e) {e.printStackTrace();}}
remove
删除第一次出现关键字为key的节点
如果链表为空不能继续删除操作
如果删除头节点,就必须改变头指针,所以要单独说明
一般情况下,需要变动cur前后结点,自然会想到:cur.prev.next = cur.next; cur.next.prev = cur.prev;
那如果是尾删呢?上面两行代码只有前面一行还能继续用,由于是尾删,尾节点就要发生改变,所以last = cur.prev;
public void remove(int key) {if(head == null) {return;}if(head.val == key) {head = head.next;if(head != null) {head.prev = null;}return;}ListNode cur = head.next;while(cur != null) {if(cur.val == key) {cur.prev.next = cur.next;if(cur.next == null) {last = cur.prev;} else {cur.next.prev = cur.prev;}return;}cur = cur.next;}}
removeAllKey
删除所有值为key的节点
删除所有的key,上面我们写了删除第一次出现key的结点,这里把代码直接帮过来,删掉return就可以继续用,但是一定是对的吗?
前面的链表判空直接返回没有问题,但是头删的话就有问题了,假设头节点是你要删除的结点就意味着头指针要发生改变,那如果新的头节点又要发生改变呢?这里我们选择尽量不改变我们的祖传代码,把头删放在最后面去做即可。
public void removeAllKey(int key) {if(head == null) {return;}ListNode cur = head.next;while(cur != null) {if(cur.val == key) {cur.prev.next = cur.next;if(cur.next == null) {last = cur.prev;} else {cur.next.prev = cur.prev;}}cur = cur.next;}if(head.val == key) {head = head.next;if(head != null) {head.prev = null;}}}
contains
是否包含key这个元素
public boolean contains(int key) {ListNode cur = head;while(cur != null) {if(cur.val == key) {return true;}cur = cur.next;}return false;}
clear
你可以直接将head 和last都置为null,这样链表就会被JVM自动回收。
这里模仿源码的写法,源码是一个一个结点都置为null,最后头尾指针再置为null
public void clear() {ListNode cur = head;while(cur != null) {ListNode tmp = cur.next;cur.prev = null;cur.next = null;cur = tmp;}head = last = null;}
LinkedList 使用
Java集合类中给我们提供了LinkedList,这是一个无头双向循环链表,我们来看一下它里面的方法,方法名字和上面我们模拟实现的差不多。
LinkedList 的构造方法
第二个构造方法是可以传入一个对象,和之前ArrayList表示二维数组是一个意思。
LinkedList 的方法
要注意LinkedList和ArrayList 的subList是一样的原理,截取的list还是原来的对象list,只是范围不同,并没有创建新的对象。
add(默认尾插)
注意LinkedList的add方默认是尾插:
public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(1);list.add(2);list.add(3);System.out.println(list);}
addAll
尾插一个对象
public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(1);list.add(2);list.add(3);System.out.println(list);ArrayList<Integer> list1 = new ArrayList<>();list1.add(10);list1.add(20);list.addAll(list1);System.out.println(list);}
遍历链表
直接打印
LinkedList也是重写了toString 方法
public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(1);list.add(2);list.add(3);System.out.println(list);}
for 循环
public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(1);list.add(2);list.add(3);int size = list.size();for (int i = 0; i < size; i++) {System.out.print(list.get(i) + " ");}System.out.println();}
for each
public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(1);list.add(2);list.add(3);for(int x : list) {System.out.print(x + " ");}System.out.println();}
迭代器
ListIterator 是继承 Iterator 的,这两个都可以来遍历链表打印数据。
迭代器的使用可以类似下面的图:
while(it.hasNext())hasNext表示是否由下一个数据,通过next()方法打印下一个数据,之后 it 一直向后移动。
Iterator
public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(1);list.add(2);list.add(3);System.out.println("===== Iterator ====");Iterator<Integer> it = list.iterator();while (it.hasNext()) {System.out.print(it.next()+" ");}System.out.println();}
ListIterator
public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(1);list.add(2);list.add(3);ListIterator<Integer> lit = list.listIterator();while (lit.hasNext()) {System.out.print(lit.next()+" ");}System.out.println();}
ListIterator(逆向遍历)
public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(1);list.add(2);list.add(3);System.out.println("===== ListIterator ====");ListIterator<Integer> lit2 = list.listIterator(list.size());while (lit2.hasPrevious()) {System.out.print(lit2.previous()+" ");}System.out.println();}
listIterator(int n) ,可以指定从哪个下标开始遍历链表
ArrrayList 和 LinkedLisrt 的总结
配套练习:
http://t.csdnimg.cn/nmObG
相关文章:

JavaDS —— 单链表 与 LinkedList
顺序表和链表区别 ArrayList : 底层使用连续的空间,可以随机访问某下标的元素,时间复杂度为O(1) 但是在插入和删除操作的时候,需要将该位置的后序元素整体往前或者向后移动,时间复杂度为O&…...
LangChain —— Message —— how to filter messages
文章目录 一、概述二、基本使用三、连成链 一、概述 在更复杂的链和代理中,我们可能会使用消息列表跟踪状态。此列表可以开始累积来自多个不同模型、说话者、子链等的消息,我们可能只想将此完整消息列表的子集传递给链/代理中的每个模型调用。 filter_me…...

conda install问题记录
最近想用代码处理sar数据,解放双手。 看重了isce这个处理平台,在安装包的时候遇到了一些问题。 这一步持续了非常久,然后我就果断ctrlc了 后面再次进行尝试,出现一大串报错,不知道是不是依赖项的问题 后面看到说mam…...
【python】IPython的使用技巧
IPython使用技巧 一、魔法命令 %timeit 用途:用于测量一段代码的执行时间,这对于评估代码的性能非常有帮助,尤其适用于需要进行性能优化和比较不同实现方式效率的场景。示例:%timeit [x**2 for x in range(1000)]扩展…...
常用知识点问答
kafka如何部署? 先说明kafka的版本如果是 2.X 版本,则要先部署 3或5 个服务器的zookeeper集群,然后在每个zookeeper服务器上部署kafka应用。如果是 3.X 版本,kafka不再依赖zookeeper,所以可以直接在java17的环境上部署…...
paddlenlp cpu windows 下测试gpt
paddlenlp 安装python3.11版本 conda create -n python311 python3.11 激活python conda activate python311 安装paddlepaddle conda install paddlepaddle3.0.0b0 -c paddle pip install paddlenlp3.0.0b0 -U -i https://pypi.tuna.tsinghua.edu.cn/simple windows下…...
uboot的功能
uboot裸机程序,uboot的核心功能是启动内核 uboot启动流程 XIP设备: 1、硬件初始化 2、读flash上面的内核,拷贝进内存 3、启动内核 非XIP设备 1、BROM程序拷贝uboot到RAM 2、执行uboot 3、硬件初始化 4、读flash上面的内核,拷贝进…...
java导出word实现
参考:Poi-tl Documentation...

Flink 提交作业的方式
首先我进行了flink单机部署,个人建议不管是学习还是开发尽量不使用 然后开始了flink自带集群部署,部署在三台服务器上,资源管理由flink集群自己管理,然后为了解决集群的单点故障问题,使用zookeeper监听事件࿰…...

JVM系列 | 垃圾收集算法
JVM系列 | 垃圾收集算法 文章目录 前言如何判断对象已"死"?引用计数法可达性分析算法可达性分析2.0版 | 引用的增强对象的消亡过程回收方法区主要回收目标:回收操作 垃圾收集算法分代收集理论 与 跨代引用假说分代收集理论跨带引用假说 垃圾收…...
深入理解Spring Boot中的事件驱动架构
深入理解Spring Boot中的事件驱动架构 大家好,我是微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿! 1. 引言 事件驱动架构在现代软件开发中越来越受欢迎,它能够提高系统的松耦合性和可扩展性。Sprin…...

Moldflow安装包下载:附网盘地址+详细教程步骤
如大家所了解的,Autodesk Moldflow仿真软件具有注塑成型仿真工具,能够帮助您验证和优化塑料零件、注塑模具和注塑成型流程。目前常用的版本有Moldflow 2019和Moldflow2023。 还没有获取Moldflow软件安装包资源的小伙伴,可以用百度云盘保存或下…...

2024辽宁省数学建模B题【钢铁产品质量优化】思路详解
2024 辽宁省大学数学建模竞赛试题 B 题 钢铁产品质量优化 由于连续退火工序中各阶段的工艺参数之间存在耦合性(加热炉的温度设定会影响后续均热与冷却温度的设定,以及带钢穿行速度),导致难以建立该工序的机理模型,从而…...

C++基础入门(上)
个人主页:C忠实粉丝 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C忠实粉丝 原创 C基础入门(上) 收录于专栏【C语法基础】 本专栏旨在分享学习C的一点学习笔记,欢迎大家在评论区交流讨论💌 目录 1. C发展历史 2. C版本…...
基于深度学习的情感分析
基于深度学习的情感分析是一种利用深度学习技术从文本数据中提取情感信息,判断文本的情感倾向(如正面、负面或中性)的方法。这项技术在市场营销、客户服务、社交媒体分析、产品评价和政治分析等领域有广泛应用。以下是对这一领域的系统介绍&a…...

mybatis 延迟加载
MyBatis的延迟加载(Lazy Loading)是一种优化技术,用于在需要时才加载关联对象或集合,从而提高性能和效率。以下是对MyBatis延迟加载的详细介绍: 延迟加载的基本概念 延迟加载是指在第一次访问对象的属性时才加载该对象…...

使用QT5.14.2开发族谱管理软件过程记录
目标缘由:出生在农村、学习了电脑技术,总有一个想法就是将老家传承下来的族谱录入电脑中,方便快速查询和长期保存。开始入手时候发现还挺有难度。 难点如下: 过去族谱纸质版书籍是民国时候印刷的、很多字都是繁体字、还有好些字…...

【QT】布局管理器
布局管理器 布局管理器1. 垂直布局2. 水平布局3. 网格布局4. 表单布局5. Spacer 布局管理器 之前使⽤ Qt 在界⾯上创建的控件, 都是通过 “绝对定位” 的⽅式来设定的;也就是每个控件所在的位置, 都需要计算坐标, 最终通过 setGeometry 或者 move ⽅式摆放过去。 …...
兼容问题---ios底部的安全距离css设置
在H5上适配安全区域:采用viewportenvconstant方案。 具体操作如下: 1. 需要将viewport设置为cover,env和constant才能生效。设置代码如下: <meta name"viewport" content"widthdevice-width,initial-scale1.…...
python JSON Lines (JSONL)的保存和读取;jsonl的数据保存和读取,大模型prompt文件保存常用格式
1. JSON Lines (JSONL)文件保存 将一个包含多个字典的列表保存为 JSON Lines (JSONL) 格式的文件,每个字典对应一个 JSONL 文件中的一行。以下是如何实现这一操作的 Python 代码 import json# 定义包含字典的列表 data [{"id": 1, "name": &qu…...

TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...

从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...

高频面试之3Zookeeper
高频面试之3Zookeeper 文章目录 高频面试之3Zookeeper3.1 常用命令3.2 选举机制3.3 Zookeeper符合法则中哪两个?3.4 Zookeeper脑裂3.5 Zookeeper用来干嘛了 3.1 常用命令 ls、get、create、delete、deleteall3.2 选举机制 半数机制(过半机制࿰…...

多模态大语言模型arxiv论文略读(108)
CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文标题:CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文作者:Sayna Ebrahimi, Sercan O. Arik, Tejas Nama, Tomas Pfister ➡️ 研究机构: Google Cloud AI Re…...
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件,用于在原生应用中加载 HTML 页面: 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...
JS设计模式(4):观察者模式
JS设计模式(4):观察者模式 一、引入 在开发中,我们经常会遇到这样的场景:一个对象的状态变化需要自动通知其他对象,比如: 电商平台中,商品库存变化时需要通知所有订阅该商品的用户;新闻网站中࿰…...

Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...
pycharm 设置环境出错
pycharm 设置环境出错 pycharm 新建项目,设置虚拟环境,出错 pycharm 出错 Cannot open Local Failed to start [powershell.exe, -NoExit, -ExecutionPolicy, Bypass, -File, C:\Program Files\JetBrains\PyCharm 2024.1.3\plugins\terminal\shell-int…...
[USACO23FEB] Bakery S
题目描述 Bessie 开了一家面包店! 在她的面包店里,Bessie 有一个烤箱,可以在 t C t_C tC 的时间内生产一块饼干或在 t M t_M tM 单位时间内生产一块松糕。 ( 1 ≤ t C , t M ≤ 10 9 ) (1 \le t_C,t_M \le 10^9) (1≤tC,tM≤109)。由于空间…...