深度解析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;
    // 创建新节点,其前一个节点是尾节点,后一个节点为null
    final 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);
    // 非尾部插入的情况
    else
        linkBefore(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;
  //处理前驱节点非空的情况
  else
      pred.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。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python画图【00】Anaconda和Pycharm和jupyter的使用

①Anaconda ②Pycharm 一、Anaconda安装步骤 1、双击安装包&#xff0c;点击next。 2、点我同意I agree 3、 4、选择需要安装的位置&#xff0c;位置可根据自己情况安装到具体位置&#xff0c;但要记住安装到了哪里。然后点击next 5、可选择加入到环境变量&#xff0c;…

Linux内核编码规范

学习linux内核或者linux驱动的人应该先掌握内核编码规范&#xff0c;这样才能更好的驾驭linux内核、驱动。 下面就从这几个方面讲解一下linux内核编码规范。 注释风格、排版风格、头文件风格、变量定义、宏定义、函数 1 注释风格 1.1 注释的原则是有助于对程序的阅读和理解&…

nmap端口扫描工具安装和使用方法

nmap&#xff08;Network Mapper&#xff09;是一款开源免费的针对大型网络的端口扫描工具&#xff0c;nmap可以检测目标主机是否在线、主机端口开放情况、检测主机运行的服务类型及版本信息、检测操作系统与设备类型等信息。本文主要介绍nmap工具安装和基本使用方法。 nmap主…

jar混淆,防止反编译,Allatori工具混淆jar包

文章目录 Allatori工具简介下载解压配置config.xml注意事项 Allatori工具简介 官网地址&#xff1a;https://allatori.com/ Allatori不仅混淆了代码&#xff0c;还最大限度地减小了应用程序的大小&#xff0c;提高了速度&#xff0c;同时除了你和你的团队之外&#xff0c;任何人…

java的XWPFDocument3.17版本学习

maven依赖 <dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>3.17</version> </dependency> 测试类&#xff1a; import org.apache.poi.openxml4j.exceptions.InvalidFormatExcep…

【第七在线】数据分析与人工智能在商品计划中的应用

随着技术的不断进步&#xff0c;数据分析和人工智能&#xff08;AI&#xff09;已经成为了现代商品计划的关键组成部分。在服装行业&#xff0c;这两项技术正在帮助企业更好地理解市场需求、优化库存管理、提高生产效率和提供更好的客户体验。本文将深入探讨数据分析和人工智能…

华为鸿蒙开发适合哪些人学习?

随着鸿蒙系统的崛起&#xff0c;越来越多的人开始关注鸿蒙开发&#xff0c;并希望成为鸿蒙开发者。然而&#xff0c;鸿蒙开发并不适合所有人&#xff0c;那么哪些人最适合学习鸿蒙开发呢&#xff1f;本文将为您总结鸿蒙开发适合的人群。 一、具备编程基础的人 学习鸿蒙开发需要…

C语言——最古老的树

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 缺乏明确的目标&#xff0c;一生将庸庸…

性能优化之资源优化

性能优化之资源优化 资源优化性能关键检测流程。浅析一下基于Unity3D 美术规则约束一、模型层面二、贴图层面三、动画层面四、声音层面&#xff1a;&#xff08;音频通用设置&#xff09;五、UI层面&#xff1a; 题外点&#xff1a;诚然在优化中&#xff0c;美术占比是很重要的…

CDN缓存技术对网站的作用有什么

CDN缓存是什么&#xff1f; CDN缓存&#xff0c;即内容分发网络缓存&#xff0c;是一种用于提高网络内容传输效率和稳定性的技术。CDN缓存可以在网络中存储大量的数据和文件&#xff0c;例如网页、图片、视频等&#xff0c;从而避免用户直接从原始服务器获取数据&#xff0c;减…

sql-labs服务器结构

双层服务器结构 一个是tomcat的jsp服务器&#xff0c;一个是apache的php服务器&#xff0c;提供服务的是php服务器&#xff0c;只是tomcat向php服务器请求数据&#xff0c;php服务器返回数据给tomcat。 此处的29-32关都是这个结构&#xff0c;不是用docker拉取的镜像要搭建一下…

如何使用kali来进行一次ddos攻击

本文章用于记录自己的学习路线&#xff0c;不用于其他任何途径! ! ! 哈喽啊&#xff01;又是好久不见&#xff0c;本博主在之前发过一个ddos攻击的介绍。 emm…虽然那篇文章也提到了ddos攻击的方式&#xff0c;但太过于简陋&#xff0c;好像也没有什么用&#xff0c;so&#…

redis主从复制(在虚拟机centos的docker下)

1.安装docker Docker安装(CentOS)简单使用-CSDN博客 2.编辑3个redis配置 cd /etc mkdir redis-ms cd redis-ms/ vim redis6379.conf vim redis6380.conf vim redis6381.conf# master #端口号 port 6379#设置客户端连接后进行任何其他指定前需要使用的密码 requirepass 12345…

力扣单调栈算法专题训练

目录 1 专题说明2 训练 1 专题说明 本博客用来计算力扣上的单调栈题目、解题思路和代码。 2 训练 题目1&#xff1a;2866美丽塔II。 解题思路&#xff1a;先计算出prefix[i]&#xff0c;表示0~i满足递增情况下&#xff0c;0~i上的元素之和最大值。然后计算出suffix[i]&#…

【数据结构】队列的使用|模拟实现|循环队列|双端队列|面试题

一、 队列(Queue) 1.1 概念 队列&#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表&#xff0c;队列具有先进先出FIFO(First In First Out) 入队列&#xff1a;进行插入操作的一端称为队尾&#xff08;Tail/Rear&#xff09; 出队列…

Oracle WebLogic Server WebLogic WLS组件远程命令执行漏洞 CVE-2017-10271

Oracle WebLogic Server WebLogic WLS组件远程命令执行漏洞 CVE-2017-10271 已亲自复现 漏洞名称漏洞描述影响版本 漏洞复现环境搭建漏洞利用 修复建议 漏洞名称 漏洞描述 在Oracle WebLogic Server 10.3.6.0.0/12.1.3.0.3/2.2.1/1.10/12.2.1.1/22.0&#xff08;Application …

Linux数据库主从复制(单主单从)

MySQL主从复制的优点包括&#xff1a; 1、横向扩展解决方案 - 在多个从站之间分配负载以提高性能。在此环境中&#xff0c;所有写入和更新都必须在主服务器上进行。但是&#xff0c;读取可以在一个或多个从设备上进行。该模型可以提高写入性能&#xff08;因为主设备专用于更新…

css 三角形实现方式及快速联想记忆

css实现三角形是常见的需求&#xff0c;在此记录如下 1 边框实现 原理&#xff1a;相邻的border之间会形成一条斜线(可按此联想记忆) .triangle {width: 0;height: 0;border-left: 100px solid red;border-right: 100px solid green;border-top: 100px solid blue;border-bot…

redis 从0到1完整学习 (六):Hash 表数据结构

文章目录 1. 引言2. redis 源码下载3. dict 数据结构4. 哈希表扩容与 rehash5. 参考 1. 引言 前情提要&#xff1a; 《redis 从0到1完整学习 &#xff08;一&#xff09;&#xff1a;安装&初识 redis》 《redis 从0到1完整学习 &#xff08;二&#xff09;&#xff1a;red…

循环渲染ForEach

目录 1、接口说明 2、键值生成规则 3、组件创建规则 3.1、首次渲染 3.2、非首次渲染 4、使用场景 4.1、数据源不变 4.2、数据源组项发生变化 4.3、数据源数组项子属性变化 5、反例 5.1、渲染结果非预期 5.2、渲染性能降低 Android开发中我们有ListView组件、GridVi…