【集合】LinkedList 详解

Java中的LinkedList是一种实现了List接口的双向链表数据结构。链表是由一系列节点(Node)组成的,每个节点包含了指向上一个节点的指针prev,数据item指向下一个节点next的指针

  • 实现了Deque接口,可以在两端进行操作(插入、删除)。并且由于LinkedList内部是基于链表实现的,所以插入、删除数据时只需要改变链表指针的指向,时间复杂度为O(1),而不需要进行数组的移动,所以它非常适合于频繁的插入、删除操作。但是LinkedList的缺点就是随机访问元素的速度较慢,因为需要从头开始遍历链表,时间复杂度为O(n)
  • 实现了Cloneable接口,支持克隆功能
  • 继承 Iterable 接口,可以使用 for-each 迭代
  • 实现了List接口,支持 增删改查 的功能

源码分析(JDK1.8)

成员变量属性

coffeescript 复制代码
/**
 * 链表节点个数
 */
transient int size = 0;

/**
 * 永远指向第一个节点
 */
transient Node<E> first;

/**
 * 永远指向最末尾的节点
 */
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;
    }
}

构造方法

LinkedList 有两个构造函数,空参构造方法,指定集合元素列表的构造方法

  • 空参构造
coffeescript 复制代码
/**
 * 构造一个空列表
 */
 public LinkedList() {
 }
  • 指定集合元素列表的构造方法
coffeescript 复制代码
/**
  *  按照集合迭代器返回的顺序,构造一个包含指定集合的元素的列表    
  */
  public LinkedList(Collection<? extends E> c) {
      this();
      addAll(c);
  }

/**
  * 将指定集合中的所有元素从此列表中的指定位置开始插入。将当前位于该位置的元素(如果有)      
  * 和任何后续元素向右移动(增加它们的索引)。新元素将按照指定集合的迭代器返回的顺序出现        
  * 在列表中。
  */
  public boolean addAll(int index, Collection<? extends E> c) {
      checkPositionIndex(index);  //校验index是否有效,是否超出链表的实际长度
  
       Object[] a = c.toArray();  //将集合转换成Object[] 数组
       int numNew = a.length;  
       if (numNew == 0) 
           return false;
  
       Node<E> pred, succ;
       // 如果索引值与链表的长度一致.那么将此指定集合中的所有元素放在链表的末尾
       if (index == size) {  
           succ = null;
           pred = last;
       } else {
       // 如果不一致,就需要将指定集合中的所有元素从此列表中的指定位置开始插入
           succ = node(index);   //从链表中获取当前index位置的节点Node
           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;
           else
               pred.next = newNode;
           pred = newNode;
       }
  
       if (succ == null) {
           last = pred;
       } else {
           pred.next = succ;
           succ.prev = pred;
       }
  
       size += numNew;   // 重新标记链表的长度
       modCount++;
       return true;
   }

add(), 插入节点方法

ArrayList 的add方法有两个,末尾添加指定位置添加

  • 末尾添加
    直接将新增节点添加在链表的最末尾处,然后指定新增节点的上一个节点prev为链表中的last节点,下一个节点next置为null
coffeescript 复制代码
/**
  * 将指定的元素追加到此列表的末尾
  */
 public boolean add(E e) {
     linkLast(e);
     return true;
 }

 /**
   * 将e链接为最后一个元素.
   */
  void linkLast(E e) {
      final Node<E> l = last;  //先获取链表最后一个节点
      //将l节点设置为e元素节点上一个节点,因为是末尾添加,所以e元素的下一个节点next为null
      final Node<E> newNode = new Node<>(l, e, null);
      last = newNode;  //先标记新增加的e元素节点为链表最末尾节点
      if (l == null) 
          //如果l节点为null,说明当前链表没有任何节点,将新增的e元素节点设置为链表头节点
          first = newNode;
      else
          //当前链表有其他节点,将l节点的next下一个节点指定为新增的e元素节点
          l.next = newNode;
      size++;   //更新链表长度+1
      modCount++;  //记录链表数据结构的变化次数
  }
  • 指定位置添加
    也叫插队添加,会打乱原本链表中节点的存储顺序。
coffeescript 复制代码
public void add(int index, E element) {
     //判断索引下标是否在链表长度范围内,超出范围则报IndexOutOfBoundsException 
     checkPositionIndex(index);
     
     if (index == size)
         //指定位置index正好等于链表的长度,那么就是尾部添加节点
         linkLast(element);
     else
         //复杂一点,见下文源码
         linkBefore(element, node(index));
 }

/**
  * 指定位置index之前添加节点
  */
 void linkBefore(E e, Node<E> succ) {
     // assert succ != null;
     final Node<E> pred = succ.prev; //获取指定位置index节点的上一个节点pred
     //将新增e元素节点的上一个节点指定为succ.prev,下一个节点next指向为succ
     final Node<E> newNode = new Node<>(pred, e, succ); 
     succ.prev = newNode; //变更succ的上一个节点为新增e元素节点
     if (pred == null)
         //表明当前链表还没任何节点,标记新增e元素节点为链表的头节点
         first = newNode;
     else
         //表明当前链表有节点,
         pred.next = newNode,将succ.prev节点的下一个节点next更细为新增e元素节点
     size++;  //更新链表长度+1
     modCount++;  //记录链表数据结构的变化次数
 }

remove(), 删除元素方法

LinkedList 的remove方法有两个,指定节点删除指定位置删除

  • 指定节点删除
    由于LinkedList 允许元素重复,所以指定元素删除方法可能存在删除多个。
coffeescript 复制代码
public boolean remove(Object o) {
     if (o == null) {
         //指定的节点元素item为null,那么就从链表的frist节点开始遍历,将所有节点之间解绑
         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;
 }

/**
  * 解绑
  */
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;   //将前一个节点prve置为null
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;  //将下一个节点next置为null
    }

    x.item = null;  //将节点元素item置为null
    size--;   //链表节点个数 -1
    modCount++;
    return element;
 }
  • 指定位置删除
    最多只会删除一个元素,如果指定位置index超出数组结构长度,报错 IndexOutOfBoundsException
coffeescript 复制代码
public E remove(int index) {
      checkElementIndex(index);  //校验index的有效性
      return unlink(node(index));  //先获取index位置的节点,然后解绑
  }

/**
  * 解绑
  */
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;   //将前一个节点prve置为null
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;  //将下一个节点next置为null
    }

    x.item = null;  //将节点元素item置为null
    size--;   //链表节点个数 -1
    modCount++;
    return element;
 }

set(), 修改节点元素方法

LinkedList 中的修改方法只有一个,通过index下标来精准修改, 修改元素节点的步骤,其实就是同一个位置的节点item的覆盖操作

,set方法只修改节点的item信息,prev和next信息不变

coffeescript 复制代码
 public E set(int index, E element) {
     checkElementIndex(index);  //校验index的有效性,IndexOutOfBoundsException
      Node<E> x = node(index); //先获取index位置的节点
      E oldVal = x.item;  
      x.item = element;  //重新赋值
      return oldVal;
    }

get(), 取元素方法

LinkedList 的get方法只有一个,只能通过索引下标index获取

coffeescript 复制代码
public E get(int index) {
      checkElementIndex(index);  //校验index的有效性,IndexOutOfBoundsException
      return node(index).item;  //核心, 有算法
  }

Node<E> node(int index) {
    // assert isElementIndex(index);
   
    if (index < (size >> 1)) {   //用index与链表长度size的一半比较,右移>>位运算
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;  //从first开始,从前往后一个个遍历直到到达index所在位置
        return x;
    } else {
        Node<E> x = last;  //从last开始,从后向前一个个遍历直到到达index所在位置
        for (int i = size - 1; i > index; i--)
            x = x.prev;  
        return x;
    }
}
相关推荐
码蜂窝编程官方3 分钟前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
Viktor_Ye19 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm21 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
一二小选手25 分钟前
【Maven】IDEA创建Maven项目 Maven配置
java·maven
J老熊31 分钟前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
猿java36 分钟前
什么是 Hystrix?它的工作原理是什么?
java·微服务·面试
AuroraI'ncoding37 分钟前
时间请求参数、响应
java·后端·spring
所待.3831 小时前
JavaEE之线程初阶(上)
java·java-ee
Winston Wood1 小时前
Java线程池详解
java·线程池·多线程·性能
手握风云-1 小时前
数据结构(Java版)第二期:包装类和泛型
java·开发语言·数据结构