Java-LinkedList源码解析(数据结构之链表)

256 阅读5分钟

1.什么是链表?

百度百科:链表是一种物理存储单元上非连续、非顺序的存储结构数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

1.1单向链表结构

image.png

一个节点存放的有数据和指向下一个节点的指针 Java代码实现节点:

private static class Node<E>{
  // 存放数据
  E data;
  // 指向下一个节点的指针
  Node<E> next;
}

那么这里就可以这样理解:

image.png

可能有点迷 我的理解就是:

链表利用一个节点存放数据,这个节点存放数据和一个指向下一个节点的指针 每个节点都会存放下一个节点的数据(指向下一个节点)这样就形成了链表

2.双向链表

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表

2.1双向链表结构

image.png 双向链表与单向链表的最大区别在于双向链表的每一个节点不仅存储指向下一个节点的指针还需要储存上一个节点的指针如果为头节点则prev为null,如果为尾节点那么next为null

Java代码实现双向链表的节点

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

3.LinkedList源码解析

LinkedList<String> linkedList = new LinkedList<>();

image.png (1)首先看看LinkedList重要的几个参数 transient int size = 0;

/**
 * 定义一个双向链表头节点
 */
transient Node<E> first;

/**
 *定义一个双向链表的尾节点
 */
transient Node<E> last;

/**
*该size记录链表数据元素个数
*/
transient int size = 0;

为什么要定义变量记录头节点和尾节点?

链表的优点之一:访问头节点和尾节点为O(1)这就是定义头节点和尾节点的原因之一

LinkedList使用代码:

LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("CHINA");
linkedList.add("BEIJING");
linkedList.add(0,"WUHAN");
linkedList.get(1);

看看构造方法:

 // 无参
 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向尾部添加元素方法源码解读:

  // LinkedList添加元素
  public boolean add(E e) {
       linkLast(e);
       return true;
   }
   
   void linkLast(E e) {
       // 首先获取到尾节点 
       /**
       为什么要获取尾节点:
      (1)可以判断该链表是否是空链表
      (2)因为双向链表的结构是 每一个节点存储下一个节点的指针(数据)和储存上一个节点的指针(数据),如果不是空链表,获取到尾节点,可以让尾节点指向新节点
       */
       final Node<E> l = last;
       // new Node(存放节点数据 指向前继节点数据   数据  指向后继节点数据 )
       // 因为你是插入进来就是尾节点所以 这里的prev的值null
       final Node<E> newNode = new Node<>(l, e, null);
       // 不管此时链表是否为空 就应该更新尾节点
       last = newNode;
       // 如果链表为空
       if (l == null)
           // 则同时更新头节点为此时进来的数据  现在头节点和尾节点都是同一个数据
           first = newNode;
       else
          // 如果链表不为空 此时就获取尾节点的指针指向新节点
           l.next = newNode;
        // 最后更新链表数据元素个数
       size++;
       modCount++;
   }
   

LinkedList向指定下标添加元素方法源码解读:

public void add(int index, E element) {
       // 验证输入下标的合法性 如果 输入的下标>size或者<0 此时就会报下标越界异常
       checkPositionIndex(index);
       // 二如果输入的下标等于size 就是向尾部添加元素 此时看上面LinkedList向尾部添加元素方法源码解读
       if (index == size)
           linkLast(element);
       else
       // 如果输入的下标为0至size之间
           linkBefore(element, node(index));
   }
   // e:此时需要添加的元素 succ:获取到输入下标的元素
   void linkBefore(E e, Node<E> succ) {
       // assert succ != null;
       // 获取到占用元素节点的prev(指向上一个节点数据)数据
       final Node<E> pred = succ.prev;
       // 定义添加元素的节点
       final Node<E> newNode = new Node<>(pred, e, succ);
       // 因为要替换所以 这里将占用元素的prev(指向上一个节点指针)的数据更新为新节点
       succ.prev = newNode;
       // 如果占用节点的prev为空 代表是头节点
       if (pred == null)
       // 如果你占用元素节点的头指针prev指向为空 那么肯定是该链表的头节点 此时更新头节点为添加的元素
           first = newNode;
       else
        // 更新占用元素的上一个数据节点的next指向新节点
           pred.next = newNode;
       size++;
       modCount++;
   }

image.png LinkedList下标获取元素数据方法源码解读:

这里需要着重的理解一下 等这里理解了配合为什么jdk1.8要将数据结构换成数组+链表+红黑树 而替换掉jdk1.7之前的数组+链表的结构

 public E get(int index) {
        // 验证你输入的下标合法性 这里就不作过多的描述
        checkElementIndex(index);
        return node(index).item;
    }
    
    
    Node<E> node(int index) {
       // 这里我个人的理解就是 
       /**
       加快查找数据的速度
       这里类似二分查找法的第一步
       就是当你查找的下标大于size的一半的时候就从尾节点开始查找 
       如果查找的下标小于sie的一半 就从最前面的头节点开始查找
       省了一半的时间
       */
        if (index < (size >> 1)) {
            // 如果你输入的下标小于size的一半 就获取头节点
            Node<E> x = first;
            // 从头节点依次遍历到你输入的下标 
            // 所以这里就产生一个重点 链表获取元素是依次遍历的 所以链表查找时间为O(n) 
            // 查找的时间取决于链表的长度
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            // 当你输入的下标大于size的一半时 就直接从最后的尾节点开始遍历
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

现在你应该知道获取链表的元素是依次遍历的 所以查询时间缓慢 尤其是当链表的长度越来越大的时候 你不是根据下标获取元素的时候 只能依次遍历 我们来看看jdk1.7之前 HashMap的数据结构(数组+链表)

image.png HashMap为了解决hash冲突 使用链地址法解决hash冲突 但是如果当hash冲突严重的时候 就会导致链表的长度会越来越长,当你获取元素的时候 如果在链表上,这个时候链表很长你获取元素就会很慢 这个就是为什么在1.8会将数据结构替换成数组+链表+红黑树(这里需要记住的就是当红黑树达到一个阈值的时候 就会转换成链表)