二十三、剖析 LinkedList

剖析 LinkedList

本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记

ArrayList随机访问效率很高,但插入和删除性能比较低;LinkedList同样实现了List接口,它的特点与ArrayList几乎正好相反。除了实现了List接口外,LinkedList还实现了DequeQueue接口,可以按照队列、栈和双端队列的方式进行操作。

2.1 基本用法

LinkedList有两个构造方法:

public LinkedList()
public LinkedList(Collection<? extends E> c)

所以,可以按照下面两种方法创建LinkedList对象:

List<String> list = new LinkedList<>();
List<String> list2 = new LinkedList<>(Arrays.asList(new String[]{"a", "b", "c"}));

ArrayList一样,LinkedList同样实现了List接口,而List接口扩展了Collection接口,Collection又扩展了Iterable接口,所有这些接口的方法都是可以使用的,使用方法与ArrayList介绍的一样。

ArrayList不同的是,LinkedList实现了队列接口Queue,所谓队列就类似于日常生活中的各种排队,特点就是先进先出,在尾部添加元素,从头部删除元素,它的接口定义为:

public interface Queue<E> extends Collection<E> {/*** Inserts the specified element into this queue if it is possible to do so* immediately without violating capacity restrictions, returning* {@code true} upon success and throwing an {@code IllegalStateException}* if no space is currently available.** @param e the element to add* @return {@code true} (as specified by {@link Collection#add})* @throws IllegalStateException if the element cannot be added at this*         time due to capacity restrictions* @throws ClassCastException if the class of the specified element*         prevents it from being added to this queue* @throws NullPointerException if the specified element is null and*         this queue does not permit null elements* @throws IllegalArgumentException if some property of this element*         prevents it from being added to this queue*/boolean add(E e);/*** Inserts the specified element into this queue if it is possible to do* so immediately without violating capacity restrictions.* When using a capacity-restricted queue, this method is generally* preferable to {@link #add}, which can fail to insert an element only* by throwing an exception.** @param e the element to add* @return {@code true} if the element was added to this queue, else*         {@code false}* @throws ClassCastException if the class of the specified element*         prevents it from being added to this queue* @throws NullPointerException if the specified element is null and*         this queue does not permit null elements* @throws IllegalArgumentException if some property of this element*         prevents it from being added to this queue*/boolean offer(E e);/*** Retrieves and removes the head of this queue.  This method differs* from {@link #poll() poll()} only in that it throws an exception if* this queue is empty.** @return the head of this queue* @throws NoSuchElementException if this queue is empty*/E remove();/*** Retrieves and removes the head of this queue,* or returns {@code null} if this queue is empty.** @return the head of this queue, or {@code null} if this queue is empty*/E poll();/*** Retrieves, but does not remove, the head of this queue.  This method* differs from {@link #peek peek} only in that it throws an exception* if this queue is empty.** @return the head of this queue* @throws NoSuchElementException if this queue is empty*/E element();/*** Retrieves, but does not remove, the head of this queue,* or returns {@code null} if this queue is empty.** @return the head of this queue, or {@code null} if this queue is empty*/E peek();
}

Queue扩展了Collection,它的主要操作有三个:

  1. 在尾部添加元素(addoffer);
  2. 查看头部元素(elementpeek),返回头部元素,但不改变队列;
  3. 删除头部元素(removepoll),返回头部元素,并且从队列中删除。

每种操作都有两种形式,有什么区别呢?区别在于,对于特殊情况的处理不同。特殊情况是指队列为空或者队列为满,为空容易理解,为满是指队列有长度大小限制,而且已经占满了。LinkedList的实现中,队列长度没有限制,但别的Queue的实现可能有。在队列为空时,elementremove会抛出异常NoSuchElementException,而peekpoll返回特殊值null;在队列为满时,add会抛出异常IllegalStateException,而offer只是返回false

除了QueueLinkedList还可以当成栈,栈也是一种常用的数据结构,与队列相反,它的特点是先进后出、后进先出,类似于一个储物箱,放的时候是一件件往上放,拿的时候则只能从上面开始拿。Java中没有单独的栈接口,栈相关方法包括在了表示双端队列的接口Deque中,主要有三个方法:

void push(E e);
E pop();
E peek();

解释如下。

  1. push表示入栈,在头部添加元素,栈的空间可能是有限的,如果栈满了,push会抛出异常IllegalStateException
  2. pop表示出栈,返回头部元素,并且从栈中删除,如果栈为空,会抛出异常NoSuchElementException
  3. peek查看栈头部元素,不修改栈,如果栈为空,返回null。

讲到栈,Java中有一个类Stack,单词意思就是栈,它也实现了栈的一些方法,如push/pop/peek等,但它没有实现Deque接口,它是Vector的子类,它增加的这些方法也通过synchronized实现了线程安全,具体就不介绍了。不需要线程安全的情况下,推荐使用LinkedListArrayDeque

栈和队列都是在两端进行操作,栈只操作头部,队列两端都操作,但尾部只添加、头部只查看和删除。结合队列和栈,有一个更为通用的操作两端的接口DequeDeque扩展了Queue,包括了栈的操作方法,此外,它还有如下更为明确的操作两端的方法:

void addFirst(E e);
void addLast(E e);
E getFirst();
E getLast();
boolean offerFirst(E e);
boolean offerLast(E e);
E peekFirst();
E peekLast();
E pollFirst();
E pollLast();
E removeFirst();
E removeLast();

xxxFirst操作头部,xxxLast操作尾部。与队列类似,每种操作有两种形式,区别也是在队列为空或满时处理不同。为空时,getXXX/removeXXX会抛出异常,而peekXXX/pollXXX会返回null。队列满时,addXXX会抛出异常,offerXXX只是返回false

栈和队列只是双端队列的特殊情况,它们的方法都可以使用双端队列的方法替代,不过,使用不同的名称和方法,概念上更为清晰。

Deque接口还有一个迭代器方法,可以从后往前遍历:

    /*** Returns an iterator over the elements in this deque in reverse* sequential order.  The elements will be returned in order from* last (tail) to first (head).** @return an iterator over the elements in this deque in reverse* sequence*/Iterator<E> descendingIterator();

简单总结下:LinkedList的用法是比较简单的,与ArrayList用法类似,支持List接口,只是,LinkedList增加了一个接口Deque,可以把它看作队列、栈、双端队列,方便地在两端进行操作。如果只是用作List,那应该用ArrayList还是LinkedList呢?我们需要了解LinkedList的实现原理,根据实际需要再作决定。

2.2 实现原理

1、内部组成

我们知道,ArrayList内部是数组,元素在内存是连续存放的,但LinkedList不是。LinkedList直译就是链表,确切地说,它的内部实现是双向链表,每个元素在内存都是单独存放的,元素之间通过链接连在一起,类似于小朋友之间手拉手一样。

为了实现链表,LinkedList定义了一个s私有静态内部类 – 节点(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;}}

节点包括实际的元素E item,但同时有两个链接,分别指向前一个节点(前驱,Node<E> prev)和后一个节点(后继,Node<E> next)。

LinkedList内部组成就是如下三个实例变量:

transient int size = 0;/*** Pointer to first node.*/
transient Node<E> first;/*** Pointer to last node.*/
transient Node<E> last;

size表示链表长度,默认为 0 0 0first指向头节点,last指向尾节点,初始值都为null

LinkedList的所有public方法内部操作的都是这三个实例变量,具体是怎么操作的?链接关系是如何维护的?我们看一些主要的方法,分别分析。

2、add 方法

add方法代码如下:

    /*** Appends the specified element to the end of this list.** <p>This method is equivalent to {@link #addLast}.** @param e element to be appended to this list* @return {@code true} (as specified by {@link Collection#add})*/public boolean add(E e) {linkLast(e);return true;}

代码中,主要调用了linkLast方法,代码如下:

    /*** Links e as last element.*/void linkLast(E e) {final Node<E> l = last;final Node<E> newNode = new Node<>(l, e, null);last = newNode;if (l == null)first = newNode;elsel.next = newNode;size++;modCount++;}

linkLast代码的基本步骤如下。

  1. 创建一个新的节点newNodellast指向原来的尾节点,如果原来链表为空,则为null。代码为:

    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    
  2. 修改尾节点last,指向新的最后节点newNode。代码为:

    last = newNode;
    
  3. 修改前节点的后向链接,如果原来链表为空,则让头节点指向新节点,否则让前一个节点的next指向新节点。代码为:

    if (l == null)first = newNode;
    elsel.next = newNode;
    
  4. 增加链表大小和修改计数器。代码为:

    size++;
    modCount++;
    

接下来,通过图示加深LinkedList添加元素过程的理解:

List<String> list = new LinkedList<String>();
list.add("a");
list.add("b");

执行完List<String> list = new LinkedList<String>()后,LinkedList内部结果如下所示:

在这里插入图片描述

添加a后,LinkedList内部结果如下所示:

在这里插入图片描述

添加b后,LinkedList内部结果如下所示:

在这里插入图片描述

可以看出,与ArrayList不同,LinkedList的内存是按需分配的,不需要预先分配多余的内存,添加元素只需分配新元素的空间,然后调节几个链接即可。

3、根据索引访问元素 get

LinkedListArrayList一样,支持根据索引值访问元素,代码如下:

    /*** Returns the element at the specified position in this list.** @param index index of the element to return* @return the element at the specified position in this list* @throws IndexOutOfBoundsException {@inheritDoc}*/public E get(int index) {checkElementIndex(index);return node(index).item;}

checkElementIndex检查索引位置的有效性,如果无效,则抛出异常,代码为:

    private void checkElementIndex(int index) {if (!isElementIndex(index))throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}/*** Tells if the argument is the index of an existing element.*/private boolean isElementIndex(int index) {return index >= 0 && index < size;}

如果index有效,则传入参数index调用node方法查找对应的节点,其item属性就指向实际元素内容,node方法的代码为:

    /*** Returns the (non-null) Node at the specified element index.*/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;}}

size>>1等于size/2,如果索引位置在前半部分(index<(size>>1)),则从头节点开始查找,否则,从尾节点开始查找。可以看出,与ArrayList明显不同,ArrayList中数组元素连续存放,可以根据索引直接定位,而在LinkedList中,则必须从头或尾顺着链接查找,效率比较低。

4、根据内容查找元素索引 indexOf

    /*** Returns the index of the first occurrence of the specified element* in this list, or -1 if this list does not contain the element.* More formally, returns the lowest index {@code i} such that* {@code Objects.equals(o, get(i))},* or -1 if there is no such index.** @param o element to search for* @return the index of the first occurrence of the specified element in*         this list, or -1 if this list does not contain the element*/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;}

代码也很简单,从头节点顺着链接往后找,如果要找的是null,则找第一个itemnull的节点,否则使用equals方法进行比较。

5、插入元素 add(int, E)

add()在尾部添加元素,如果在头部或者中间插入元素呢?可以使用add(int index, E element)方法,代码如下:

    /*** Inserts the specified element at the specified position in this list.* Shifts the element currently at that position (if any) and any* subsequent elements to the right (adds one to their indices).** @param index index at which the specified element is to be inserted* @param element element to be inserted* @throws IndexOutOfBoundsException {@inheritDoc}*/public void add(int index, E element) {checkPositionIndex(index);if (index == size)linkLast(element);elselinkBefore(element, node(index));}

如果indexsize,添加到最后面,一般情况,是插入到index对应节点的前面,调用方法为linkBefore,它的代码为:

    /*** Inserts element e before non-null Node succ.*/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++;}

参数succ表示后继节点,变量pred表示前驱节点,目标是在predsucc中间插入一个节点。插入步骤是:

  1. 新建一个节点newNode,前驱为pred,后继为succ。代码为:

    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    
  2. 让后继的前驱指向新节点。代码为:

    succ.prev = newNode;
    
  3. 让前驱的后继指向新节点,如果前驱为空,那么修改头节点指向新节点。代码为:

    if (pred == null)first = newNode;
    elsepred.next = newNode;
    
  4. 增加长度和修改计数器。

我们通过图示来进行介绍。还是上面的例子,比如,插入一个元素:

list.add(1, "c");

内存结构如下图所示:

在这里插入图片描述

可以看出,在中间插入元素,LinkedList只需按需分配内存,修改前驱和后继节点的链接,而ArrayList则可能需要分配很多额外空间,且移动所有后续元素。

6、删除元素 remove(int) 和 remove(Object)

我们再来看删除元素,首先看remove(int),代码如下为:

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

remove(Object),代码如下:

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

查看代码,可以知道,删除某个节点时,都是先找到待删除的节点,随后调用unlink(Node)方法,代码如下:

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

删除x节点,基本思路就是让x的前驱和后继直接链接起来,nextx的后继,prevx的前驱,具体分为两步。

  1. x的前驱的后继指向x的后继。如果x没有前驱,说明删除的是头节点,则修改头节点指向x的后继。
  2. x的后继的前驱指向x的前驱。如果x没有后继,说明删除的是尾节点,则修改尾节点指向x的前驱。

通过图示进行说明。还是上面的例子,如果删除一个元素:

list.remove(1);

内存结构如下图所示:

在这里插入图片描述

2.3 LinkedList 特点分析

用法上,LinkedList是一个List,但也实现了Deque接口,可以作为队列、栈和双端队列使用。实现原理上,LinkedList内部是一个双向链表,并维护了长度、头节点和尾节点,这决定了它有如下特点。

  1. 按需分配空间,不需要预先分配很多空间。
  2. 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为 O ( N 2 ) O(\frac{N}{2}) O(2N)
  3. 不管列表是否已排序,只要是按照内容查找元素,效率都比较低,必须逐个比较,效率为 O ( N ) O(N) O(N)
  4. 在两端添加、删除元素的效率很高,为 O ( 1 ) O(1) O(1)
  5. 在中间插入、删除元素,要先定位,效率比较低,为 O ( N ) O(N) O(N),但修改本身的效率很高,效率为 O ( 1 ) O(1) O(1)

理解了LinkedListArrayList的特点,就能比较容易地进行选择了,如果列表长度未知,添加、删除操作比较多,尤其经常从两端进行操作,而按照索引位置访问相对比较少,则LinkedList是比较理想的选择。


  1. 马俊昌.Java编程的逻辑[M].北京:机械工业出版社,2018. ↩︎

  2. 尚硅谷教育.剑指Java:核心原理与应用实践[M].北京:电子工业出版社,2023. ↩︎

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

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

相关文章

从第一原理看大语言模型

大模型基础框架 大模型幻觉问题 大模型能力 思维链模式 思维链模式激发的是大模型的推理能力 LLM知识能力RAG

leetcode hot100 每日温度

在本题中&#xff0c;我们是通过单调栈来解决的&#xff0c;因为我们采用了栈的数据结构&#xff0c;并且&#xff0c;栈内存储的元素是单调的。 本题我们考虑&#xff0c;将气温数组元素的下标存入栈中&#xff0c;首先初始化要把0放入&#xff0c;0是下标的意思。然后我们拿…

实用工具:实时监控服务器CPU负载状态并邮件通知并启用开机自启

作用&#xff1a;在服务器CPU高负载时发送邮件通知 目录 一、功能代码 二、配置开机自启动该监控脚本 1&#xff0c;配置自启脚本 2&#xff0c;启动 三、功能测试 一、功能代码 功能&#xff1a;在CPU负载超过预设置的90%阈值时就发送邮件通知&#xff01;邮件内容显示…

【LeetCode:225. 用队列实现栈 + 栈 | 队列】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

有人吐槽:可视化大屏面向领导的设计,真相是这样吗?

某些老铁的态度很极端&#xff0c;看到可视化大屏页面就一口断定&#xff0c;除了讨好领导之外&#xff0c;屁用没有。真相是这样吗&#xff1f;贝格前端工场尝试给老铁们分析下。 一、可视化大屏确实要面向领导&#xff0c;但不是讨好领导 可视化大屏的设计需要考虑领导和管理…

如何使用grafana 下JSON API访问展示接口数据

一.新增connection 点击左侧菜单栏&#xff0c;选择Add new connection 下载安装即可。 二. 增加对应url和参数 1. 添加新的数据源 2. 配置对应url 3.新建仪表盘和添加接口url和参数等

Vins-Moon配准运行

Vins-Moon运行 求助&#xff01;&#xff01;&#xff01;源码地址电脑配置环境配置编译Kitti数据集制作IMU时间戳问题 适配Kitti数据集运行结果Euroc数据集kitti数据集 evo评估&#xff08;KITTI数据&#xff09;输出轨迹(tum格式)结果 求助&#xff01;&#xff01;&#xff…

React Switch用法及手写Switch实现

问&#xff1a;如果注册的路由特别多&#xff0c;找到一个匹配项以后还会一直往下找&#xff0c;我们想让react找到一个匹配项以后不再继续了&#xff0c;怎么处理&#xff1f;答&#xff1a;<Switch>独特之处在于它只绘制子元素中第一个匹配的路由元素。 如果没有<Sw…

【Prometheus】基于Altertmanager发送告警到多个接收方、监控各种服务、pushgateway

基于Altertmanager发送报警到多个接收方 一、配置alertmanager-发送告警到qq邮箱1.1、告警流程1.2、告警设置【1】邮箱配置【2】告警规则配置【3】 部署prometheus【4】部署service 二、配置alertmanager-发送告警到钉钉三、配置alertmanager-发送告警到企业微信3.1、注册企业微…

vue3基础教程(1)——nodejs环境搭建

博主个人小程序已经上线&#xff1a;【中二少年工具箱】 小程序二维如下&#xff1a; 正文开始 专栏简介1. 环境菜单2.为什么下载node3. nodejs简介4. nodejs安装5. 编辑器选择 专栏简介 本系列文章由浅入深&#xff0c;从基础知识到实战开发&#xff0c;非常适合入门同学。…

MySQL 面试题

MySQL 基础 数据库的约束与范式&#xff1f; 七大约束&#xff1a; 检查约束&#xff1a;以数据类型以及数据的长度进行约束&#xff0c;在一个表中&#xff0c; 所插入的数据&#xff0c;必须和数据类型匹配&#xff0c;并且范围不能超过指定的长度。非空约束 not null&…

第十四天-网络爬虫基础

目录 1.什么是爬虫 2.网络协议 OSI七层参考模型 TCP/IP模型 1.应用层 2.传输层 3.网络层 3.HTTP协议 1.介绍 2.http版本&#xff1a; 3.请求格式 4.请求方法 5.HTTP响应 状态码&#xff1a; 6.http如何连接 4.Python requests模块 1.安装 2.使用get/post 3.响…

动态规划5,粉刷房子,买卖股票的最佳时期

粉刷房子 思路&#xff1a; 1.经验题目要求 dp[i][0] 表示&#xff1a;粉刷到 i 位置的时候&#xff0c;最后一个位置粉刷上红色&#xff0c;此时的最小花费。 dp[i][1] 表示&#xff1a;粉刷到 i 位置的时候&#xff0c;最后一个位置粉刷上蓝色&#xff0c;此时的最小花费。…

java学习(常用类)

一、包装类&#xff08;针对八种基本数据类型相应的引用类型--包装类. 1)包装类和基本数据类型的相互转换 装箱&#xff1a;基本类型->包装类型 拆箱&#xff1a;包装类型->基本类型 //以下是int类型和char类型演示。 public class temp1 {public static void main(St…

TOMCAT的安装与基本信息

一、TOMCAT简介 Tomcat 服务器是一个免费的开放源代码的Web 应用服务器&#xff0c;属于轻量级应用服务器&#xff0c;在中小型系统和并发访问用户不是很多的场合下被普遍使用&#xff0c;是开发和调试JSP 程序的首选。对于一个初学者来说&#xff0c;可以这样认为&#xff0c…

Rust调用同级目录中的rs文件和调用下级目录中的rs文件

一、Rust调用同级目录中的rs文件 Rust新建工程demo02&#xff0c;src文件夹下面新建test.rs文件&#xff0c;这样main.rs文件与它属于同级目录中。 关键点&#xff1a;导入test文件和test文件中的Ellipse模块 mod test;//导入test模块&#xff08;文件&#xff09; use test…

MySQL-MHA搭建、故障测试

一、架构说明 MHA&#xff08;Master High Availability&#xff09;是一个用于 MySQL 主从复制管理和自动故障转移的开源工具集。MHA 的主要目的是提供 MySQL 环境的高可用性和自动故障转移功能&#xff0c;确保在主库发生故障时能够快速切换到备库&#xff0c;降低业务中断时…

Android 性能优化--APK加固(1)混淆

文章目录 为什么要开启混淆如何开启代码混淆如何开启资源压缩代码混淆配置代码混淆后&#xff0c;Crash 问题定位结尾 本文首发地址&#xff1a;https://h89.cn/archives/211.html 最新更新地址&#xff1a;https://gitee.com/chenjim/chenjimblog 为什么要开启混淆 先上一个 …

架构设计:生产消费模型

1. 引言 在现代软件系统中&#xff0c;处理大量数据和消息是一项重要的任务。生产消费模型作为一种经典的并发模式&#xff0c;在解决数据生产和消费之间的关系上发挥着关键作用。该模型通过有效地管理生产者和消费者之间的通信和数据流动&#xff0c;实现了系统组件之间的解耦…

LASSO算法

LASSO (Least Absolute Shrinkage and Selection Operator) 是一种回归分析的方法&#xff0c;它能够同时进行变量选择和正则化&#xff0c;以增强预测准确性和模型的解释性。LASSO通过在损失函数中加入一个L1惩罚项来实现这一点。该惩罚项对系数的绝对值进行约束。 基本概念 …