深度解析LinkedList

LinkedList是Java集合框架中List接口的实现之一,它以双向链表的形式存储元素。与传统的数组相比,链表具有更高的灵活性,特别适用于频繁的插入和删除操作。让我们从底层实现开始深入了解这个强大的数据结构。

linkedList.jpg

底层数据结构

LinkedList的底层数据结构是双向链表。每个节点都包含一个数据元素以及两个引用,一个指向前一个节点(prev),一个指向下一个节点(next)。这种结构使得在链表中进行插入和删除操作变得非常高效。

linkedList.png

LinkedList的属性及Node源码如下:

public class LinkedList<E>extends AbstractSequentialList<E>implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{transient int size = 0;transient Node<E> first;transient Node<E> last;public LinkedList() {}...private static class Node<E> {E item;Node<E> next;Node<E> prev;Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}}...}

LinkedList包含两个重要的实例变量:first和last,分别指向链表的头节点和尾节点。这两个节点使得在链表的两端进行操作更为高效。

size字段表示链表中元素的数量,通过它我们可以随时获取链表的大小。

Node类是LinkedList数据结构的基础。每个节点都包含一个数据元素、一个指向下一个节点的引用(next),以及一个指向前一个节点的引用(prev)。

  • item:当前节点的数据元素。
  • next:下一个节点的引用。
  • prev:前一个节点的引用。

操作方法

篇幅有限,我们在这详细解释下常用的几个方法,别的方法家人们可自行阅读源码

add(E e): 在链表尾部添加元素

add(E e): 在链表尾部添加元素。

// LinkedList类中的add方法
public boolean add(E e) {linkLast(e);return true;
}

linkLast(e)

// 在链表尾部链接新节点的方法
void linkLast(E e) {// 获取尾节点的引用final Node<E> l = last;// 创建新节点,其前一个节点是尾节点,后一个节点为nullfinal Node<E> newNode = new Node<>(l, e, null);// 将新节点更新为尾节点last = newNode;if (l == null)// 如果链表为空,同时将新节点设置为头节点first = newNode;else// 否则,将原尾节点的next指向新的尾节点l.next = newNode;// 增加链表的大小size++;//修改计数器modCount++;
}

源码详解:

  • final Node l = last;

    通过last字段获取链表的尾节点引用。这一步是为了后续创建新节点时能够将其连接到链表的尾部。

  • final Node newNode = new Node<>(l, e, null);

    使用Node类的构造方法创建一个新节点,其前一个节点是链表的尾节点l,后一个节点为null,因为这是新的尾节点。

  • last = newNode;

将链表的last字段更新为新创建的节点,使其成为新的尾节点。

  • if (l == null) first = newNode;

    如果链表为空(即尾节点为null),则将头节点first指向新节点。因为在空链表中添加元素时,头节点和尾节点都是新节点。

  • else l.next = newNode;

如果链表非空,将原尾节点的next引用指向新节点,以完成新节点的插入。

  • size++;

    每次成功添加一个元素后,增加链表的大小。

  • modCount++;

    modCount是用于迭代器的修改计数器,用于在迭代时检测是否有其他线程修改了集合。每次对链表结构进行修改时,都会增加modCount的值。modCount是用于迭代器的修改计数器,用于在迭代时检测是否有其他线程修改了集合。每次对链表结构进行修改时,都会增加modCount的值。

add(int index, E element): 在指定位置插入元素

  //在指定位置插入元素的方法public void add(int index, E element) {//参数检查checkPositionIndex(index);//链表尾部插入元素if (index == size)linkLast(element);// 非尾部插入的情况elselinkBefore(element, node(index));
}

checkPositionIndex():参数检查

//参数检查
private void checkPositionIndex(int index) {if (!isPositionIndex(index))throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

isPositionIndex():判断指定下标是否合法

//判断指定下标是否合法
private boolean isPositionIndex(int index) {return index >= 0 && index <= size;
}

node(int index):获取指定位置的节点

Node<E> node(int index) {// assert isElementIndex(index);//判断索引位置(判断索引位于链表的前半部分还是后半部分,提高元素获取的性能)if (index < (size >> 1)) {//前半部分的话从头节点开始遍历,通过节点的next一直查找到当前索引所在的元素Node<E> x = first;for (int i = 0; i < index; i++)x = x.next;return x;} else {//后半部分的话从尾始遍历,通过节点的prev一直查找到当前索引所在的元素                         Node<E> x = last;for (int i = size - 1; i > index; i--)x = x.prev;return x;}
}

源码详解:

  • if (index < (size >> 1)) {

>>带符号右移运算符,一个数的二进制表示向右移动指定的位数,左侧空出的位置使用原始数值的最高位进行填充。这个操作相当于将数值除以2的指定次方并向下取整。右移一位相当于除以2。

这行代码是判断索引位置,即判断索引位于链表的前半部分还是后半部分来决定是从前往后还是从后往前遍历链表,以提高元素获取的性能。

  • Node x = first; for (int i = 0; i < index; i++) x = x.next;

如果目标节点在链表的前半部分,就从头节点 first 开始,通过next往后遍历,找到对应索引的节点并返回。

  • Node x = last; for (int i = size - 1; i > index; i–) x = x.prev;

    如果目标节点在链表的后半部分,就从尾节点 last 开始,通过prev往前遍历,找到对应索引的节点并返回。

linkBefore():非尾部插入元素

void linkBefore(E e, Node<E> succ) {// assert succ != null;//获取到要插入位置元素的前驱引用final Node<E> pred = succ.prev;// 创建新节点,其前驱引用是插入位置原节点的前驱引用,后驱引用为插入位置原节点final Node<E> newNode = new Node<>(pred, e, succ);//更新插入位置原节点的前驱引用为插入节点succ.prev = newNode;//处理前驱节点为空的情况if (pred == null)first = newNode;//处理前驱节点非空的情况elsepred.next = newNode;// 增加链表的大小size++;//修改计数器modCount++;
}                                       

源码详解:

  • final Node pred = succ.prev;

    通过 succ 节点的 prev 引用,获取插入位置的前一个节点 pred。

  • final Node newNode = new Node<>(pred, e, succ);

    使用 Node 类的构造方法创建一个新的节点,其前一个节点是 pred,后一个节点是 succ。

  • succ.prev = newNode;

    将后继节点 succ 的前驱引用指向新节点 newNode,确保新节点的插入。

  • if (pred == null) first = newNode;

    如果前驱节点为空,说明新节点插入的位置是链表的头部,将链表的头节点 first 指向新节点 newNode。

  • else pred.next = newNode;

    如果前驱节点非空,将前驱节点的 next 引用指向新节点 newNode,完成新节点的插入。

  • size++;

每次成功添加一个元素后,增加链表的大小。

  • modCount++;

modCount是用于迭代器的修改计数器,用于在迭代时检测是否有其他线程修改了集合。每次对链表结构进行修改时,都会增加modCount的值。modCount是用于迭代器的修改计数器,用于在迭代时检测是否有其他线程修改了集合。每次对链表结构进行修改时,都会增加modCount的值。

remove(Object o): 从链表中移除指定元素

public boolean remove(Object o) {//处理删除元素为null的情况if (o == null) {//遍历链表for (Node<E> x = first; x != null; x = x.next) {//获取到第一个为null的元素if (x.item == null) {//删除元素unlink(x);return true;}}//处理删除元素非null的情况} else {//遍历链表for (Node<E> x = first; x != null; x = x.next) {//获取到要删除的元素if (o.equals(x.item)) {//删除元素unlink(x);return true;}}}return false;
}

源码解析:

  • if (o == null) {

这里首先检查传入的参数 o 是否为 null,分别处理 null 和非 null 两种情况。

  • if (o == null) { for (Node x = first; x != null; x = x.next) {…

如果要删除的元素是 null,则通过遍历链表找到第一个值为 null 的节点,然后调用 unlink(x) 方法删除该节点。删除成功后返回 true。如果要删除的元素是 null,则通过遍历链表找到第一个值为 null 的节点,然后调用 unlink(x) 方法删除该节点。删除成功后返回 true。

  • else { for (Node x = first; x != null; x = x.next) { …

如果要删除的元素不为 null,则通过遍历链表找到第一个值与参数 o 相等的节点,然后调用 unlink(x) 方法删除该节点。删除成功后返回 true。

  • return false;

如果遍历完整个链表都没有找到要删除的元素,则返回 false 表示删除失败。

unlink(Node x):实际执行节点删除的方法

E unlink(Node<E> x) {// assert x != null;//获取要删除的元素final E element = x.item;//获取要删除的元素的后继final Node<E> next = x.next;//获取要删除的元素的前驱final Node<E> prev = x.prev;//处理前驱节点为空的情况if (prev == null) {first = next;//前驱节点非空则处理前驱的后继} else {prev.next = next;x.prev = null;}//处理后继节点为空的情况if (next == null) {last = prev;//后继节点非空则处理后继的前驱} else {next.prev = prev;x.next = null;}//清空目标节点的数据元素x.item = null;//减小链表的大小size--;//更新修改计数器modCount++;return element;
}

源码详解:

  • final E element = x.item;

    通过 x 节点的 item 字段获取节点的数据元素,即要删除的元素。

  • final Node next = x.next; final Node prev = x.prev;

通过 x 节点的 next 和 prev 字段获取目标节点的后继节点和前驱节点。

  • if (prev == null) { if (prev == null) { first = next; } else { prev.next = next; x.prev = null; }

    如果前驱节点为空,说明要删除的节点是链表的头节点,将目标节点的后继节点 next设置为链表的头节点 first 。如果前驱节点非空,将前驱节点的 next 引用指向目标节点的后继节点,同时将目标节点的 prev 引用置为 null。

  • if (next == null) { last = prev; else { next.prev = prev; x.next = null; }

如果后继节点为空,说明要删除的节点是链表的尾节点,将链表的尾节点 last 指向目标节点的前驱节点 prev。如果后继节点非空,将后继节点的 prev 引用指向目标节点的前驱节点,同时将目标节点的 next 引用置为 null。

  • x.item = null;

将目标节点的 item 字段置为 null,帮助垃圾回收系统回收节点的数据元素。

  • size–; modCount++;

    每次成功删除一个节点后,减小链表的大小,并更新修改计数器。

  • return element;

最后,返回被删除节点的数据元素。

LinkedList的优势和劣势

优势

  • 动态大小: 链表可以动态地分配内存,不需要预先指定大小。
  • 插入和删除: 在链表中插入和删除元素更为高效,因为只需要调整节点的引用。

劣势

  • 随机访问困难: 在链表中要访问特定位置的元素,必须从头结点开始遍历,效率相对较低。
  • 额外空间: 链表每个节点需要额外的空间存储引用,相比数组会占用更多的内存。

使用场景

LinkedList适用于以下场景:

  • 频繁的插入和删除操作: 由于链表的节点可以方便地插入和删除,适用于这类操作频繁的场景。
  • 不需要频繁随机访问: 如果主要操作是在链表两端进行,而不是在中间进行随机访问,那么链表是一个不错的选择。

总结

LinkedList作为Java集合框架中的一个重要成员,为开发者提供了一种灵活而高效的数据结构。通过深入了解其底层实现和基本特性,我们能够更好地在实际应用中选择和使用这一数据结构,从而优化程序的性能和效率。希望这篇文章能够帮助你更好地理解和使用LinkedList。

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

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

相关文章

天猫数据分析(软件工具)-2023年11月天猫保健品行业分析报告:市场需求扩容,年轻人是主流群体

近年来&#xff0c;随着健康经济、颜值经济的兴起&#xff0c;越来越多的年轻人加入养生大军&#xff0c;成为保健食品市场上的一股新力量&#xff0c;带动市场扩容。根据鲸参谋电商数据分析平台的相关数据显示&#xff0c;今年11月份&#xff0c;天猫平台上保健食品的销量为24…

YOLOv8改进 | 主干篇 | 利用MobileNetV3替换Backbone(轻量化网络结构)

一、本文介绍 本文给大家带来的改进机制是MobileNetV3&#xff0c;其主要改进思想集中在结合硬件感知的网络架构搜索&#xff08;NAS&#xff09;和NetAdapt算法&#xff0c;以优化移动设备CPU上的性能。它采用了新颖的架构设计&#xff0c;包括反转残差结构和线性瓶颈层&…

Unity | HybridCLR 热更新(Windows端)

目录 一、准备工作 1.环境相关 2.Unity中配置 二、热更新 1.创建 HotUpdate 热更新模块 2.安装和配置HybridCLR 3.配置PlayerSettings 4.创建热更新相关脚本 5.打包dll 6.测试热更新 一、准备工作 1.环境相关 安装git环境。Win下需要安装visual studio 2019或更高版…

使用Aspose.Slides 控件,在线将 ODP 转换为 PPT

OpenOffice 等开源生产力工具有其用途。但如果您希望在线将 ODP 转换为 PPT&#xff0c;您很可能已经确定 Microsoft PowerPoint 的专有 PPT 格式和平台比 OpenOffice ODP 更适合您的需求。 本文的第一部分重点介绍在线将 ODP 转换为 PPT 的快速方法。第二部分探讨涉及C#应用程…

R语言中使用ggplot2绘制散点图箱线图,附加显著性检验

散点图可以直观反映数据的分布&#xff0c;箱线图可以展示均值等关键统计量&#xff0c;二者结合能够清晰呈现数据蕴含的信息。 本篇笔记主要内容&#xff1a;介绍R语言中绘制箱线图和散点图的方法&#xff0c;以及二者结合展示教程&#xff0c;添加差异比较显著性分析&#xf…

27.Java程序设计-基于Springboot的在线考试系统小程序设计与实现

1. 引言 随着数字化教育的发展&#xff0c;在线考试系统成为教育领域的一项重要工具。本论文旨在介绍一个基于Spring Boot框架的在线考试系统小程序的设计与实现。在线考试系统的开发旨在提高考试的效率&#xff0c;简化管理流程&#xff0c;并提供更好的用户体验。 2. 系统设…

UG阵列特征

阵列特征&#xff1a;将一个或多个特征&#xff0c;沿线性方向阵列复制图形 实体建模时建议草图尽可能简单&#xff0c;能特征阵列的别草图阵列 阵列特征命令在如下位置&#xff1a;菜单-插入-关联复制-阵列特征 当我们只需要选中的特征沿着一个或两个方向进行阵列的时候&…

Day68力扣打卡

打卡记录 得到山形数组的最少删除次数&#xff08;线性DP 前后缀分解&#xff09; 链接 class Solution:def minimumMountainRemovals(self, nums: List[int]) -> int:n len(nums)pre, suf [1] * n, [1] * nfor i in range(n):for j in range(i):if nums[j] < nums[…

Go 随机密码

一.Go实现随机密码 随机密码 package mainimport ("fmt""math/rand""os""strconv""time" )func RandomPassword(num int) {length : numif len(os.Args) > 1 {arg : os.Args[1]i, err : strconv.ParseInt(arg, 10, 6…

HarmonyOS - macOS 上搭建 鸿蒙开发环境

文章目录 安装 DevEco第一个 App1、工程基本信息设置2、安装设备3、运行工程 安装 DevEco 软件下载地址&#xff1a; https://developer.harmonyos.com/cn/develop/deveco-studio 今天我下载 DevEco Studio 3.1.1 Release - Mac 版本 解压后是一个 dmg 文件&#xff08;也不必…

Grafana高可用-LDAP

一. grafana高可用 1. 迁移之前的 grafana sqlitedump.sh #!/bin/bash DB$1 TABLES$(sqlite3 $DB .tables | sed -r s/(\S)\s(\S)/\1\n\2/g | grep -v migration_log) for t in $TABLES; doecho "TRUNCATE TABLE $t;" done for t in $TABLES; doecho -e ".mode…

Linux中vim中进行替换/批量替换

Linux中vim中进行替换/批量替换 一:在 Vim 中进行文本替换的操作是通过使用 :s&#xff08;substitute&#xff09;命令来实现的。这里是一些基本的替换命令 替换当前行的第一个匹配项: :s/old/new/这将替换当前行中第一个出现的 “old” 为 “new”。 替换当前行的所有匹配项…

Linux ContOS7 日志管理(rsyslog)

目录 01. rsyslog 记录日志程序 02.日志文件 03.日志等级 Linux 日志文件是记录 Linux 系统运行信息的文件。它们类似于人类的日记&#xff0c;记录了系统的各种活动&#xff0c;如用户登录、进程启动、错误消息等。 Linux 日志文件通常存储在 /var/log/ 目录中。该目录包含…

Linux应用程序管理(rpm yum 源码安装)

一.Linux应用程序基础 当我们主机安装Linux操作系统时候&#xff0c;也会同时安装一些软件或网络服务等等&#xff0c;但是随着系统一起安装的软件包毕竟他是少数的&#xff0c;能够实现的功能也是有限的&#xff0c;如果需要实现更丰富的功能&#xff0c;那就需要安装应用程序…

构建数字化金融生态系统:云原生的创新方法

内容来自演讲&#xff1a;曾祥龙 | DaoCloud | 解决方案架构师 摘要 本文探讨了金融企业在实施云原生体系时面临的挑战&#xff0c;包括复杂性、安全、数据持久化、服务网格使用和高可用容灾架构等。针对网络管理复杂性&#xff0c;文章提出了Spiderpool开源项目&#xff0c;…

The Cherno C++笔记 03

目录 Part 07 How the C Linker Works 1.链接 2.编译链接过程中出现的错误 2.1 缺少入口函数 注意:如何区分编译错误还是链接错误 注意&#xff1a;入口点可以自己设置 2.2 找不到自定义函数 2.2.1缺少声明 2.2.2自定义函数与引用函数不一致 2.3 在头文件中放入定义 …

git入门以及如何推送代码到云端

Gitee&#xff08;码云&#xff09;是开源中国于2013年推出的基于Git的代码托管平台、企业级研发效能平台&#xff0c;提供中国本土化的代码托管服务。 地址&#xff1a; Gitee - 基于 Git 的代码托管和研发协作平台 步骤1&#xff1a;创建远程仓库 在Gitee上创建一个新的远…

c# OpenCV 检测(斑点检测、边缘检测、轮廓检测)(五)

在C#中使用OpenCV进行图像处理时&#xff0c;可以使用不同的算法和函数来实现斑点检测、边缘检测和轮廓检测。 斑点检测边缘检测轮廓检测 一、斑点检测&#xff08;Blob&#xff09; 斑点检测是指在图像中找到明亮或暗的小区域&#xff08;通常表示为斑点&#xff09;&#…

java类和对象的思想概述

0.面向对象Object OOP——名人名言&#xff1a;类是写出来的&#xff0c;对象是new出来的 **> 学习面向对象的三条路线 java类以及类成员&#xff1a;&#xff08;重点&#xff09;类成员——属性、方法、构造器、&#xff08;熟悉&#xff09;代码块、内部类面向对象特征&…

【论文解读】CNN-Based Fast HEVC Quantization Parameter Mode Decision

时间&#xff1a;2019 年 级别&#xff1a;SCI 机构&#xff1a;南京信息工程大学 摘要 随着多媒体呈现技术、图像采集技术和互联网行业的发展&#xff0c;远程通信的方式已经从以前的书信、音频转变为现在的音频/视频。和 视频在工作、学习和娱乐中的比例不断提高&#xff0…