Linkedlist源码详解

news/2024/11/16 6:40:51/文章来源:https://www.cnblogs.com/seven97-top/p/18397421

介绍

LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack)。这样看来,LinkedList简直就是个全能冠军。当你需要使用栈或者队列时,可以考虑使用LinkedList,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue的类(它是个接口名字,无法直接创建)。关于栈或队列,现在的首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)有着更好的性能。

对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高;底层使用双向链表存储

//存储链表的第一个节点
transient Node<E> first;//存储链表的最后一个节点
transient Node<E> last;

LinkedList的实现方式决定了所有跟下标相关的操作都是线性时间,而在首段或者末尾删除元素只需要常数时间。为追求效率LinkedList没有实现同步(synchronized),如果需要多个线程并发访问,可以先采用Collections.synchronizedList()方法对其进行包装。

底层实现

底层数据结构

LinkedList底层通过双向链表实现。双向链表的每个节点用内部类Node表示。LinkedList通过first和last引用分别指向链表的第一个和最后一个元素。注意这里没有所谓的哑元(也就是没有虚拟变量),当链表为空的时候first和last都指向null。

transient int size = 0;/*** Pointer to first node.* Invariant: (first == null && last == null) ||*            (first.prev == null && first.item != null)*/
transient Node <E> first;/*** Pointer to last node.* Invariant: (first == null && last == null) ||*            (last.next == null && last.item != null)*/
transient Node <E> last;

其中Node是私有的内部类:

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;}
}

构造函数

 /*** Constructs an empty list.*/public LinkedList() {}/*** Constructs a list containing the elements of the specified* collection, in the order they are returned by the collection's* iterator.** @param  c the collection whose elements are to be placed into this list* @throws NullPointerException if the specified collection is null*/public LinkedList(Collection <? extends E> c) {this();addAll(c);}

getFirst(), getLast()

获取第一个元素, 和获取最后一个元素:

/*** Returns the first element in this list.** @return the first element in this list* @throws NoSuchElementException if this list is empty*/
public E getFirst() {final Node <E> f = first;if (f == null)throw new NoSuchElementException();return f.item;
}/*** Returns the last element in this list.** @return the last element in this list* @throws NoSuchElementException if this list is empty*/
public E getLast() {final Node <E> l = last;if (l == null)throw new NoSuchElementException();return l.item;
}

get方法

get方法是根据索引获取元素的

public E get(int index) {checkElementIndex(index);return node(index).item;
}//找到具体索引位置的  Node
Node <E> node(int index) {// assert isElementIndex(index);//类似二分法,size >> 1 即元素数量的一半if (index < (size >> 1)) { //索引小于一半,就从头开始找Node <E> x = first;for (int i = 0; i < index; i++)x = x.next;return x;} else { //否则就从尾部开始找Node <E> x = last;for (int i = size - 1; i > index; i--)x = x.prev;return x;}
}

removeFirst(), removeLast(), remove(e), remove(index)

remove()方法也有两个版本

  • 删除跟指定元素相等的第一个元素remove(Object o)

  • 删除指定下标处的元素remove(int index)

删除元素:指的是删除第一次出现的这个元素, 如果没有这个元素,则返回false;判断的依据是equals方法, 如果equals,则直接unlink这个node;
由于LinkedList可存放null元素,故也可以删除第一次出现null的元素;

public boolean remove(Object o) {if (o == null) {for (Node <E> x = first; x != null; x = x.next) {if (x.item == null) {unlink(x);return true;}}} else {for (Node <E> x = first; x != null; x = x.next) {if (o.equals(x.item)) {unlink(x);return true;}}}return false;
}/*** Unlinks non-null 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; // GCsize--;modCount++;return element;
}

linklist是有序的,可重复存储数据的,并且可以存储null值,但remove方法在找到需要删除的元素时,就return了,因此只能删除找到的第一个元素

LinkedList<String> list = new LinkedList();
list.add("1");
list.add(null);
list.add(null);
list.add("1");
System.out.println(list);
list.remove(null);
list.remove("1");
System.out.println(list);
//输出:
[1, null, null, 1]
[null, 1]

add方法

add()方法有两个版本:

  • add(E e),该方法在LinkedList的末尾插入元素,因为有last指向链表末尾,在末尾插入元素的花费是常数时间。只需要简单修改几个相关引用即可;

  • add(int index, E element),该方法是在指定下表处插入元素,需要先通过线性查找找到具体位置,然后修改相关引用完成插入操作。

public boolean add(E e) {linkLast(e);//默认add方法是在尾部插入元素return true;
}void linkLast(E e) {final Node<E> l = last;//旧的尾部元素final Node<E> newNode = new Node<>(l, e, null);last = newNode;//尾部元素为 newNodeif (l == null)//说明集合中没有元素first = newNode;elsel.next = newNode;//旧的尾部元素  指向 newNode,即添加newNodesize++;modCount++;
}//同理。链表头部添加元素private void linkFirst(E e) {final Node<E> f = first;final Node<E> newNode = new Node<>(null, e, f);first = newNode;if (f == null)last = newNode;elsef.prev = newNode;size++;modCount++;
}

add(int index, E element),当index==size时,等同于add(E e); 如果不是,则分两步:

  1. 先根据index找到要插入的位置,即node(index)方法;
  2. 修改引用,完成插入操作。
public void add(int index, E element) {checkPositionIndex(index);if (index == size)linkLast(element);elselinkBefore(element, node(index));
}

上面代码中的node(int index)函数有一点小小的trick,因为链表双向的,可以从开始往后找,也可以从结尾往前找,具体朝那个方向找取决于条件index < (size >> 1),也即是index是靠近前端还是后端。从这里也可以看出,linkedList通过index查询元素的效率没有arrayList高。

Node <E> node(int index) {// assert isElementIndex(index);if (index < (size >> 1)) {Node <E> x = first;for (int i = 0; i < index; i++)x = x.next;return x;} else {Node <E> x = last;for (int i = size - 1; i > index; i--)x = x.prev;return x;}
}

addAll()

addAll(index, c) 实现方式并不是直接调用add(index,e)来实现,主要是因为效率的问题,另一个是fail-fast中modCount只会增加1次;

 public boolean addAll(Collection <? extends E> c) {return addAll(size, c);}public boolean addAll(int index, Collection <? extends E> c) {checkPositionIndex(index);Object[] a = c.toArray();int numNew = a.length;if (numNew == 0)return false;Node <E> pred, succ;if (index == size) {succ = null;pred = last;} else {succ = node(index);pred = succ.prev;}for (Object o: a) {@SuppressWarnings("unchecked") E e = (E) o;Node <E> newNode = new Node <> (pred, e, null);if (pred == null)first = newNode;elsepred.next = newNode;pred = newNode;}if (succ == null) {last = pred;} else {pred.next = succ;succ.prev = pred;}size += numNew;modCount++;return true;}

clear()

这里是将node之间的引用关系都变成null,这样GC可以在年轻代回收元素了

public void clear() {// Clearing all of the links between nodes is "unnecessary", but:// - helps a generational GC if the discarded nodes inhabit//   more than one generation// - is sure to free memory even if there is a reachable Iteratorfor (Node <E> x = first; x != null;) {Node <E> next = x.next;x.item = null;x.next = null;x.prev = null;x = next;}first = last = null;size = 0;modCount++;
}

注意:有些同学可能注意到这里的判定条件是x != null,但是 LinkedList 是可以设置null值的,那是不是当链表中有null的元素,并且爱clear遍历到元素为null时,条件 x != null就为false了,那就不会继续执行,也就是后续的元素都无法置为空了?
实际上不是的,这里的x != null是没有这个元素,而LinkedList 中可以设置null值是指,x.item = null ,也就是有Node,但是Node的item属性为null

Positional Access 方法

通过index获取元素

 public E get(int index) {checkElementIndex(index);return node(index).item;}

将某个位置的元素重新赋值:

public E set(int index, E element) {checkElementIndex(index);Node <E> x = node(index);E oldVal = x.item;x.item = element;return oldVal;
}

将元素插入到指定index位置:

 public void add(int index, E element) {checkPositionIndex(index);if (index == size)linkLast(element);elselinkBefore(element, node(index));}

删除指定位置的元素:

 public E remove(int index) {checkElementIndex(index);return unlink(node(index));}

其它位置的方法:

 private boolean isElementIndex(int index) {return index >= 0 && index < size;}private boolean isPositionIndex(int index) {return index >= 0 && index <= size;}private String outOfBoundsMsg(int index) {return "Index: " + index + ", Size: " + size;}private void checkElementIndex(int index) {if (!isElementIndex(index))throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}private void checkPositionIndex(int index) {if (!isPositionIndex(index))throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}

查找操作

查找操作的本质是查找元素的下标:

查找第一次出现的index, 如果找不到返回-1;

public int indexOf(Object o) {int index = 0;if (o == null) {for (Node <E> x = first; x != null; x = x.next) {if (x.item == null)return index;index++;}} else {for (Node <E> x = first; x != null; x = x.next) {if (o.equals(x.item))return index;index++;}}return -1;
}

查找最后一次出现的index, 如果找不到返回-1;

public int lastIndexOf(Object o) {int index = size;if (o == null) {for (Node <E> x = last; x != null; x = x.prev) {index--;if (x.item == null)return index;}} else {for (Node <E> x = last; x != null; x = x.prev) {index--;if (o.equals(x.item))return index;}}return -1;}

LinkedList 存在的性能问题

  • 从速度的角度:ArrayDeque 基于数组实现双端队列,而 LinkedList 基于双向链表实现双端队列,数组采用连续的内存地址空间,通过下标索引访问,链表是非连续的内存地址空间,通过指针访问,所以在寻址方面数组的效率高于链表。

  • 从内存的角度:虽然 LinkedList 没有扩容的问题,但是插入元素的时候,需要创建一个 Node 对象, 换句话说每次都要执行 new 操作,当执行 new 操作的时候,其过程是非常慢的,会经历两个过程:类加载过程 、对象创建过程。

    • 类加载过程

      • 会先判断这个类是否已经初始化,如果没有初始化,会执行类的加载过程

      • 类的加载过程:加载、验证、准备、解析、初始化等等阶段,之后会执行 <cinit> 方法,初始化静态变量,执行静态代码块等等

    • 对象创建过程

      • 如果类已经初始化了,直接执行对象的创建过程

      • 对象的创建过程:在堆内存中开辟一块空间,给开辟空间分配一个地址,之后执行初始化,会执行 <init>方法,初始化普通变量,调用普通代码块

关于作者

来自一线程序员Seven的探索与实践,持续学习迭代中~

本文已收录于我的个人博客:https://www.seven97.top

公众号:seven97,欢迎关注~

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

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

相关文章

创建一个SpringBoot项目,实现简单的CRUD功能和分页查询

背景 本博文主要是创建了一个新的SpringBoot项目,实现基本的增删改查,分页查询,带条件的分页查询功能。是方便初学者学习后端项目的一个比较清晰明了的实践代码,读者可根据博文,从自己动手创建一个新的SpringBoot项目,到使用PostMan测试基本请求,完完全全实践一遍,写出…

每天5分钟复习OpenStack(十五)Ceph与Bcache结合

上一章我们成功部署了bcache,这一章我们将Ceph与Bcache结合来使用,使用Bcache来为ceph的数据盘提速。1 ceph 架构 一个标准的ceph集群可能是如下的架构,SSD/NVME 存储元数据,而SATA盘存储数据。这样的架构下,物理介质的SATA盘读写速率上限决定了存储集群Ceph的上限(木桶效…

青岛-烟台-威海攻略

烟台 朝阳街-烟台山-所城里朝阳街 漫心酒店外小熊烟台山 烟台开埠陈列馆 烟台京剧艺术馆 冰心纪念馆 烟台地标 烟台山灯塔 旗袍博物馆所城里 宣化城墙金沙滩-滨海中路-渔人码头金沙滩城市展示中心 新城北街养马岛 可以在岛上租电驴 需要一整天酒店推荐烟台站附近 Day1 芝罘湾广…

探索Matplotlib-Gallery:Python数据可视化的游乐园

探索matplotlib-gallery:Python数据可视化的游乐园在数据科学的世界里,数据可视化是一个不可或缺的工具,它帮助我们理解数据、发现模式、并传达信息。Matplotlib是Python中最强大的数据可视化库之一,而其Gallery则是一个展示各种可视化技巧和图表类型的宝库。今天,我们将一…

freeRTOS源码解析4--task.c 2

4、task.c解析 时隔两年,还是决定继续把这个系统解析完成,有始有终。不过这次源码又从官网上下载了最新的,可能和我以前看的略有区别,但应该基本不影响理解。 接下来正式开始。4.1.3 新增或是遗漏的两个宏1 /* Returns pdTRUE if the task is actively running and not…

Charles - 夜神模拟器证书安装App抓包-charles监控手机出现unknown 已解决

1.Openssl安装 http://slproweb.com/products/Win32OpenSSL.html exe下载安装后进行配置 新建系统变量OPENSSL_HOME,变量值设为(绝对路径)软件安装目录下的bin 直接浏览 编辑用户变量path,新建%OPENSSL_HOME%,最后点击确定 查看openssl版本,输入命令:openssl version2.夜…

基于迭代扩展卡尔曼滤波算法的倒立摆控制系统matlab仿真

1.课题概述基于迭代扩展卡尔曼滤波算法的倒立摆控制系统,对比UKF,EKF迭代UKF,迭代EKF四种卡尔曼滤波的控制效果。2.系统仿真结果 3.核心程序与模型 版本:MATLAB2022a%迭代扩展卡尔曼滤波 X_iukf = zeros(2, Times1); X_iukf(:,1) = state0; P_iukf = zeros(2…

支持大模型的小模型

https://www.arxiv.org/pdf/2408.12748 (SLM Meets LLM: Balancing Latency, Interpretability and Consistency in Hallucination Detection )平衡会话 AI 幻觉检测中的延迟、可解释性和一致性 介绍 大型语言模型(llm)在实时任务(如同步的会 话 ui)中与延迟作斗争。 当额外的…

基于颜色模型和边缘检测的火焰识别FPGA实现,包含testbench和matlab验证程序

1.算法运行效果图预览 (完整程序运行后无水印)将FPGA仿真结果导入到matlab显示结果:测试样本1测试样本2测试样本32.算法运行软件版本 vivado2019.2matlab2022a3.部分核心程序 (完整版代码包含注释和操作步骤视频)`timescale 1ns / 1ps // // Company: // Engineer: // //…

exkmp/Z函数

扩展 KMP/exKMP(Z 函数) 首先我们求出 \(ne\) 数组。代表 \(b\) 与 \(b\) 的每一个后缀的最长公共前缀长度。 我们设当前要求 \(ne_x\),且 \(k\) 为使得 \(p=k+ne_k-1\) 最大的位置且 \(0\le k<x\)。于是我们得到了两个蓝块相同。再通过这个图,得出两个绿块相等。

群晖搭建个人图书馆

概述 本文依赖于 github 项目 talebook (https://github.com/talebook ) 本文依赖于 github 项目(GitHub - cxfksword/douban-api-rs: 简单的豆瓣api,主要用于在jellyfin中刮削电影信息) 其实就是面向于新手的个人图书馆项目搭建,但是作者本人也是新手[/笑哭]。 搭建 tale…

业务类中处理点赞、收藏和浏览量

在对点赞、收藏和浏览量进行操作时,获取分布式锁,以确保并发情况下数据的准确性。 先更新缓存,然后将更新数据库的操作放入消息队列中异步处理,以提高响应速度。 缓存预热 在系统启动或定期将热门文章的相关数据加载到缓存中,减少首次访问时的数据库查询。描述 点赞、收藏…