源码角度简单介绍LinkedList

LinkedList是一种常见的数据结构,但是大多数开发者并不了解其底层实现原理,以至于存在很多误解,在这篇文章中,将带大家一块深入剖析LinkedList的源码,并为你揭露它们背后的真相。首先想几个问题,例如:

  1. LinkedList 的底层是基于什么数据结构实现的?
  2. LinkedList 的插入和删除操作时间复杂度是否都是 O(1) ?
  3. LinkedList 和 ArrayList 相比,哪种结构存储数据的时候更占内存?
  4. LinkedList 真的不支持随机访问吗?
  5. LinkedList 是线程安全的吗?

接下来一块分析一下 LinkedLis t的源码,看完 LinkedList 源码之后,可以轻松解答上面几个问题 

简介 

LinkedList底层是基于双向链表实现的,内部有三个属性,size用来存储元素个数,first指向链表头节点,last指向链表尾节点。

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

 头尾节点都是由Node节点组成,Node节点表示双向链表,内部结构如下:

private static class Node<E> {// 存储元素数据E item;// 后继节点,指向下一个元素LinkedList.Node<E> next;// 前驱节点,指向上一个元素LinkedList.Node<E> prev;// 构造函数Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {this.item = element;this.next = next;this.prev = prev;}
}

再看一下LinkedList的继承类图,还是很清晰的:

 LinkedList实现了List接口,提供了集合操作的常用方法,当然也包含随机访问的方法,只不过没有相ArrayList那样实现RandomAccess接口,LinkedList提供的随机访问的方法时间复杂度并不是常量级别的。

public interface List<E> extends Collection<E> {// 查询方法int size();boolean isEmpty();boolean contains(Object o);Iterator<E> iterator();Object[] toArray();<T> T[] toArray(T[] a);// 修改方法boolean add(E e);boolean remove(Object o);// 批量修改方法boolean containsAll(Collection<?> c);boolean addAll(Collection<? extends E> c);boolean addAll(int index, Collection<? extends E> c);boolean removeAll(Collection<?> c);boolean retainAll(Collection<?> c);default void replaceAll(UnaryOperator<E> operator) {}default void sort(Comparator<? super E> c) {}void clear();// 比较方法boolean equals(Object o);int hashCode();// 随机访问方法E get(int index);E set(int index, E element);void add(int index, E element);E remove(int index);// 搜索方法int indexOf(Object o);int lastIndexOf(Object o);// 迭代方法ListIterator<E> listIterator();ListIterator<E> listIterator(int index);java.util.List<E> subList(int fromIndex, int toIndex);
}

LinkedList还实现了Deque接口,Deque是 double ended queue 的缩写,读音是 ['dek] ,读错就尴尬了。 Deque是双端队列,可以在头尾进行插入和删除操作,兼具栈和队列的性质。 内部结构如下:

public interface Deque<E> extends Queue<E> {// 基础方法void addFirst(E e);void addLast(E e);boolean offerFirst(E e);boolean offerLast(E e);E removeFirst();E removeLast();E pollFirst();E pollLast();E getFirst();E getLast();E peekFirst();E peekLast();boolean removeFirstOccurrence(Object o);boolean removeLastOccurrence(Object o);// 队列方法boolean add(E e);boolean offer(E e);E remove();E poll();E element();E peek();// 栈方法void push(E e);E pop();// 集合方法boolean remove(Object o);boolean contains(Object o);public int size();Iterator<E> iterator();Iterator<E> descendingIterator();
}

Deque为什么提供了这么多增删查的方法?为了满足不同的使用场景。比如Deque队列已经满了,再往里面添加元素,addFirst() 方法会抛出异常,offerFirst() 方法会返回false

初始化

LinkedList只有一个构造方法,无参构造方法,并不能像ArrayList那样指定长度。

List<Integer> list = new LinkedList<>();
//构造方法
public LinkedList() {
}

添加元素

添加元素的方法根据位置区分,共有三种,在头部添加、在尾部添加和在任意位置添加。

在头部添加 addFirst/push offerFirst 在尾部添加 addLast add/offer/offerLast 在任意位置添加 add(index, e)

先看一下使用的最多的add(e)方法底层实现:

// 添加元素
public boolean add(E e) {// 在末尾添加元素linkLast(e);return true;
}// 在末尾添加元素
void linkLast(E e) {// 1. 获取尾节点final LinkedList.Node<E> l = last;// 2. 初始化新节点final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);// 3. 追加到末尾last = newNode;if (l == null) {first = newNode;} else {l.next = newNode;}size++;modCount++;
}

可以看到add(e

// 添加元素
public void push(E e) {// 在头部添加元素addFirst(e);
}// 在头部添加元素
public void addFirst(E e) {linkFirst(e);
}// 在头部添加元素,底层私有实现
private void linkFirst(E e) {// 1. 获取头节点final LinkedList.Node<E> f = first;// 2. 初始化新节点final LinkedList.Node<E> newNode = new LinkedList.Node<>(null, e, f);// 3. 追加到头部first = newNode;if (f == null) {last = newNode;} else {f.prev = newNode;}size++;modCount++;
}

)方法是尾部添加元素,再看一个从头部添加元素的push()。

最后看一个在任意位置添加到方法add(index, e)的底层实现:

// 在下标index位置添加元素
public void add(int index, E element) {// 检查下标是否越界checkPositionIndex(index);// 如果index等于链表的最后一个元素,则添加到末尾if (index == size) {linkLast(element);} else {// 添加到指定位置前面(先找到index位置的元素)linkBefore(element, node(index));}
}// 在当前元素前面添加新元素
void linkBefore(E e, LinkedList.Node<E> succ) {final LinkedList.Node<E> pred = succ.prev;// 创建新节点,并将新节点插入到当前节点之前final LinkedList.Node<E> newNode = new LinkedList.Node<>(pred, e, succ);succ.prev = newNode;if (pred == null) {first = newNode;} else {pred.next = newNode;}size++;modCount++;
}

再看一下检查下标是否越界的方法底层实现:

/ 检查下标是否越界
private void checkElementIndex(int index) {if (!isElementIndex(index)) {throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}
}// 判断下标是否越界
private boolean isElementIndex(int index) {return index >= 0 && index < size;
}

查询元素

查询元素的方法跟位置区分,共有三种,查询头节点、查询尾节点和查询任意位置元素。

方法含义 如果不存在则返回null 如果不存在则抛异常 

查询头部 peek/peekFirst getFirst/element 

查询尾部 peekLast getLast 

查询任意位置 - get

看一下从头查询的element()方法的底层实现:

// 查询元素
public E element() {return getFirst();
}// 获取第一个元素
public E getFirst() {final LinkedList.Node<E> f = first;if (f == null) {throw new NoSuchElementException();}return f.item;
}

再看一个查询尾节点的方法getLast()的底层实现:

// 获取最后一个元素
public E getLast() {final LinkedList.Node<E> l = last;if (l == null) {throw new NoSuchElementException();}return l.item;
}

再看一个查询任意位置的方法get(index)的底层实现:

// 查询下标是index位置的元素
public E get(int index) {// 检查下标是否越界checkElementIndex(index);// 返回对应下标的元素return node(index).item;
}// 返回对应下标的元素
LinkedList.Node<E> node(int index) {// 判断下标是否落在前半段if (index < (size >> 1)) {// 如果在前半段,则从头开始遍历LinkedList.Node<E> x = first;for (int i = 0; i < index; i++) {x = x.next;}return x;} else {// 如果在后半段,则从尾开始遍历LinkedList.Node<E> x = last;for (int i = size - 1; i > index; i--) {x = x.prev;}return x;}
}

 可见LinkedList的也支持随机访问,只不过时间复杂度是O(n)。

删除元素 

删除元素的方法按照位置区分,也分为三种,分别是删除头节点、删除尾节点和删除任意位置节点。

方法含义  返回布尔值(如果不存在,返回false) 返回旧值(如果不存在则抛异常)

 从头部删除 remove(o)/removeFirstOccurrence  remove/poll/pollFirst/removeFirst/pop 

从尾部删除 removeLastOccurrence pollLast/removeLast 

从任意位置删除 - remove(index) 

先看一个从头开始删除的方法remove()的底层实现: 

// 删除元素
public E remove() {// 删除第一个元素return removeFirst();
}// 从头删除元素
public E removeFirst() {final LinkedList.Node<E> f = first;if (f == null) {throw new NoSuchElementException();}// 调用实际的删除方法return unlinkFirst(f);
}// 删除第一个元素
private E unlinkFirst(LinkedList.Node<E> f) {final E element = f.item;final LinkedList.Node<E> next = f.next;// 断开头节点与后继节点的连接f.item = null;f.next = null;first = next;if (next == null) {last = null;} else {next.prev = null;}size--;modCount++;return element;
}

 再看一个从最后一个节点开始删除的方法removeLast()的底层实现:

// 删除最后一个元素
public E removeLast() {final LinkedList.Node<E> l = last;if (l == null) {throw new NoSuchElementException();}// 实际的删除逻辑return unlinkLast(l);
}// 删除最后一个元素
private E unlinkLast(LinkedList.Node<E> l) {final E element = l.item;// 断开与前一个节点的连接final LinkedList.Node<E> prev = l.prev;l.item = null;l.prev = null;last = prev;if (prev == null) {first = null;} else {prev.next = null;}size--;modCount++;return element;
}

再看一个从任意位置的节点开始删除的方法remove(index)的底层实现:

// 删除下标是index位置的元素
public E remove(int index) {// 检查下标是否越界checkElementIndex(index);// 删除下标对应的元素(先找到下标对应的元素)return unlink(node(index));
}// 删除下标对应的元素
E unlink(LinkedList.Node<E> x) {final E element = x.item;// 1. 备份当前节点的前后节点final LinkedList.Node<E> next = x.next;final LinkedList.Node<E> prev = x.prev;// 2. 断开与前驱节点的连接if (prev == null) {first = next;} else {prev.next = next;x.prev = null;}// 3. 断开与后继节点的连接if (next == null) {last = prev;} else {next.prev = prev;x.next = null;}x.item = null;size--;modCount++;return element;
}

 总结

学完了LinkedList的核心方法的源码,现在可以很容易回答文章开头的几个问题了。

  1. LinkedList的底层是基于什么数据结构实现的?

答案:双向链表。

  1. LinkedList的插入和删除操作时间复杂度是否都是 O(1) ?

答案:不是,在头尾操作的时间复杂度是O(1),在其他位置操作的时间复杂度是O(n)。

  1. LinkedList和ArrayList相比,哪种结构存储数据的时候更占内存?

答案:由于LinkedList的每个节点还包含前后节点的引用,所以会占用更多的空间。

  1. LinkedList真的不支持随机访问吗?

答案:LinkedList支持随机访问,比如get(index)和get(o)方法,不过它们的时间复杂度是O(n)。

  1. LinkedList是线程安全的吗?

答案:LinkedList不是线程安全的,内部没有提供同步机制来保证线程安全。并发修改的时候可能导致数据错乱,在遍历过程中修改会抛出ConcurrentModificationException异常。 想要线程安全,其中一种方式是初始化List的时候使用 Collections.synchronizedList() 修饰。这样LinkedList所有操作都变成同步操作,性能较差。还有一种性能较好,又能保证线程安全的方式是使用 CopyOnWriteArrayList

// 第一种方式,使用 Collections.synchronizedList() 修饰
List<Integer> list = Collections.synchronizedList(new LinkedList<>());// 第二种方式,使用 CopyOnWriteArrayList
List<Integer> list = new CopyOnWriteArrayList<>();

同样因为 LinkedList有队列的相关属性,也可以当做队列来使用,并且可以实现简单的LRU算法来进行数据淘汰的方式。

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

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

相关文章

C++初阶-string类的模拟实现

string类的模拟实现 一、经典的string类问题1.1 构造函数1.1.1 全缺省的构造函数 2.1 拷贝构造3.1 赋值4.1 析构函数5.1 c_str6.1 operator[]7.1 size8.1 capacity9.1 比较&#xff08;ASCII&#xff09;大小10.1 resize11.1 reserve12.1 push_back(尾插字符)13.1 append(尾插字…

MIT18.06线性代数 笔记3

文章目录 对称矩阵及正定性复数矩阵和快速傅里叶变换正定矩阵和最小值相似矩阵和若尔当形奇异值分解线性变换及对应矩阵基变换和图像压缩单元检测3复习左右逆和伪逆期末复习 对称矩阵及正定性 特征值是实数特征向量垂直>标准正交 谱定理&#xff0c;主轴定理 为什么对称矩…

PaddleOCR:超越人眼识别率的AI文字识别神器

在当今人工智能技术已经渗透到各个领域。其中&#xff0c;OCR&#xff08;Optical Character Recognition&#xff09;技术将图像中的文字转化为可编辑的文本&#xff0c;为众多行业带来了极大的便利。PaddleOCR是一款由百度研发的OCR开源工具&#xff0c;具有极高的准确率和易…

Python从入门到精通七:Python函数进阶

函数多返回值 学习目标&#xff1a; 知道函数如何返回多个返回值 问: 如果一个函数如些两个return (如下所示)&#xff0c;程序如何执行&#xff1f; 答&#xff1a;只执行了第一个return&#xff0c;原因是因为return可以退出当前函数&#xff0c;导致return下方的代码不执…

(3)kylin系统部署weblogic项目

一、jdk迁移 1、拷贝成功后要配置环境变量 vi /etc/profile 将jdk的目录添加进去 2、将jdk安装目录拷贝后权限会发生变化&#xff0c; 要对jdk下bin目录中的所有文件修改权限&#xff1a; chmod x ./* 回车即可 ----------------------------- 环境变量 export …

DBeaver连接kingbase8(人大金仓)

DBeaver连接kingbase8(人大金仓) 1、添加驱动 步骤&#xff1a;选择"数据库-->驱动管理器" 类名&#xff1a;com.kingbase8.Driver URL模板&#xff1a;jdbc:kingbase8://{host}[:[{post}]/[{database}] 端口&#xff1a;54321 添加jar包 2、连接数据库 点击…

Python 进阶(十六):二进制和ASCII码的转换(binascii 模块)

大家好&#xff0c;我是水滴~~ 本文详细介绍了Python中的binascii模块及其使用方法。通过binascii模块&#xff0c;我们可以方便地进行二进制和ASCII字符串之间的转换操作。文章中包含大量的示例代码&#xff0c;希望能够帮助新手同学快速入门。 《Python入门核心技术》专栏总…

【OPENGIS】Geoserver升级Jetty,不修改java版本

昨天搞了一个geoserver升级9.4.53版本的方法&#xff0c;但是需要修改java的版本&#xff0c;因为jetty官方网站下载的jar包是用jdk11编译的&#xff0c;如果不升级java版本&#xff0c;运行就会报错。 可是现场环境限制比较多&#xff0c;升级了java版本之后有些老版本的程序又…

【模拟】LeetCode-48. 旋转图像

旋转图像。 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6]…

Python 进阶(十五):Base64 编码和解码(base64 模块)

大家好&#xff0c;我是水滴~~ 本篇文章主要介绍Python的base64模块&#xff0c;主要内容有&#xff1a;Base64的概念、base64模块、base64编码和解码、以及其使用场景。文章中包含大量的示例代码&#xff0c;希望能够帮助新手同学快速入门。 《Python入门核心技术》专栏总目录…

ardupilot开发 --- git 篇

一些概念 工作区&#xff1a;就是你在电脑里能看到的目录&#xff1b;暂存区&#xff1a;stage区 或 index区。存放在 &#xff1a;工作区 / .git / index 文件中&#xff1b;版本库&#xff1a;本地仓库&#xff0c;存放在 &#xff1a;工作区 / .git 中 关于 HEAD 是所有本地…

逆序对的数量

归并排序模板题 相关文章 //采用归并排序,归并的过程可以算出逆序对的个数//所有的逆序对个数 /*排序后,两个数都在左边的逆序对数排序后,两个数都在右边的逆序对数如果一个数在左边,一个数在右边,在归并的过程中*/ //左边 < 右边,正常归并。如果左边 > 右边 //那么左边…

【头歌系统数据库实验】实验9 SQL视图

目录 第1关&#xff1a;请为三建工程项目建立一个供应情况的视图V_SPQ&#xff0c;包括供应商代码(SNO)、零件代码(PNO)、供应数量(QTY) 第2关&#xff1a;从视图V_SPQ找出三建工程项目使用的各种零件代码及其数量 第3关&#xff1a;从视图V_SPQ找出供应商S1的供应情况 第4…

2024世界燕窝滋补品展|上海燕博会推荐品牌天健燕窝集团-为消费者带来好燕窝!

天健燕窝集团拥有27年燕窝进出口贸易经验。是最早加入经营正规燕窝业务的企业之一&#xff0c;业务范围遍布全中国&#xff0c;2015 年至2019 年连续5年燕窝进口量全国第一。 一年一届的世界燕窝及天然滋补品博览会暨世界滋补生态发展大会&#xff08;简称上海燕博会&#xff…

网格中的最小路径代价

说在前面 &#x1f388;不知道大家对于算法的学习是一个怎样的心态呢&#xff1f;为了面试还是因为兴趣&#xff1f;不管是出于什么原因&#xff0c;算法学习需要持续保持。 问题描述 给你一个下标从 0 开始的整数矩阵 grid &#xff0c;矩阵大小为 m x n &#xff0c;由从 0 …

VUE3语法--toRefs与toRef用法

1、功能概述 ref和reactive能够定义响应式的数据&#xff0c;当我们通过reactive定义了一个对象或者数组数据的时候&#xff0c;如果我们只希望这个对象或者数组中指定的数据响应&#xff0c;其他的不响应。这个时候我们就可以使用toRefs和toRef实现局部数据的响应。 toRefs是…

CentOS7 安装包 MariaDB 10.4.x

CentOS7 安装包 MariaDB 10.4.x 统一 MariaDB安装包 https://www.alipan.com/s/fvLg3gN7LPX 提取码: nh81 打开「阿里云盘」

关于Anaconda的安装和环境部署(此章专为新手制定)

目录 Anaconda简介 一、软件下载&#xff08;地址&#x1f447;&#xff09; 2&#xff1a;点击下载 3&#xff1a;版本选择&#xff1a; 4&#xff1a;Anaconda的安装包就下载完成了 2&#xff1a;恭喜你&#xff0c;看到这里已经完成安装了 三、部署环境 1&#xff1…

什么是 AWS IAM?如何使用 IAM 数据库身份验证连接到 Amazon RDS(上)

驾驭云服务的安全环境可能很复杂&#xff0c;但 AWS IAM 为安全访问管理提供了强大的框架。在本文中&#xff0c;我们将探讨什么是 AWS Identity and Access Management (IAM) 以及它如何增强安全性。我们还将提供有关使用 IAM 连接到 Amazon Relational Database Service (RDS…

ubuntu 20.04 server 安装 zabbix

ubuntu 20.04 server 安装 zabbix 参考文档 https://www.yuque.com/fenghuo-tbnd9/ffmkvs?# zabbix没用过&#xff0c;用过prometheus&#xff0c; 因为现在很多应用都支持直接接入prometheus监控&#xff0c; 而且大部分语言都都有sdk支持&#xff0c; 可以直接接入自己的…