Java 链表 LinkedList

链表


线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。分为:单循环链表、双向循环链表。


单链表


优点:

  • 随意进行增删改
  • 插入删除效率高
  • 长度可以随意修改

缺点:

  • 内存不连续
  • 不能随机查找

双链表(LinkedList)


优点:

  • 随意进行增删改
  • 插入效率高
  • 删除效率高
  • 长度可以随意修改
  • 查找效率比单链表快一倍

缺点:

  • 内存不连续,不能随机查找,但是效率比单链表快一倍

首先我们看到了LinkedList间接的实现了List接口(说明LinkedList是有list的特性的,add,remove等)、实现了Cloneable(可复制)、Serializable(进行了序列化),除此之外还有一个东西还实现了Queue(队列,说明应该是有队列的一些特性,pop等),这次先不侧重queue的东西,我们主要看LinkedList是如何实现链表的。

public class LinkedList extends AbstractSequentialList 
    implements List, Deque, Cloneable, java.io.Serializable {
    //链表的长度
    transient int size = 0;
    //链表的头
    transient Node first;
    //链表的尾
    transient Node last;

    //链表维护的Node的结构
    private static class Node {
        E item;
        Node next;
        Node prev;

        Node(Node prev, E element, Node next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
}

链表维护了一个size、first(头)和last(尾),我们可以看到Node的结构中有个item、next、prev充分说明了此链表是一个双向链表。

addAll(Collection c)

构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。该构造函数首先会调用LinkedList(),构造一个空列表,然后调用了addAll()方法将Collection中的所有元素添加到列表中。以下是addAll()的源代码:

/**
* 添加指定 collection 中的所有元素到此列表的结尾,
* 顺序是指定 collection 的迭代器返回这些元素的顺序。
*/
public boolean addAll(Collection c) {
    return addAll(size, c);
}
    
 /**
 * 将指定 collection 中的所有元素从指定位置开始插入此列表。
 * 其中index表示在其中插入指定collection中第一个元素的索引
 */
public boolean addAll(int index, Collection c) {
    //若插入的位置小于0或者大于链表长度,则抛出IndexOutOfBoundsException异常
    if (index < 0 || index > size)
        throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
    Object[] a = c.toArray();
    int numNew = a.length;    //插入元素的个数
    //若插入的元素为空,则返回false
    if (numNew == 0)
        return false;
    //modCount:在AbstractList中定义的,表示从结构上修改列表的次数
    modCount++;
    //获取插入位置的节点,若插入的位置在size处,则是头节点,否则获取index位置处的节点
    Entry successor = (index == size ? header : entry(index));
    //插入位置的前一个节点,在插入过程中需要修改该节点的next引用:指向插入的节点元素
    Entry predecessor = successor.previous;
    //执行插入动作
    for (int i = 0; i < numNew; i++) {
        //构造一个节点e,这里已经执行了插入节点动作同时修改了相邻节点的指向引用
        //
        Entry e = new Entry((E) a[i], successor, predecessor);
        //将插入位置前一个节点的下一个元素引用指向当前元素
        predecessor.next = e;
        //修改插入位置的前一个节点,这样做的目的是将插入位置右移一位,
        //保证后续的元素是插在该元素的后面,确保这些元素的顺序
        predecessor = e;
    }
    successor.previous = predecessor;
    //修改容量大小
    size += numNew;
    return true;
}

在addAll()方法中,涉及到了两个方法,一个是entry(int index),该方法为LinkedList的私有方法,主要是用来查找index位置的节点元素。

 /**
 * 返回指定位置(若存在)的节点元素
 */
private Entry entry(int index) {
    if (index < 0 || index >= size)
        throw new IndexOutOfBoundsException(
        "Index: " + index + ", Size: " + size);
            // 取出头结点
        Entry e = header;
        // size>>1右移一位代表除以2,这里使用简单的二分方法,判断index与list的中间位置的距离
        if (index < (size >> 1)) {
            // 如果index距离list中间位置较近,则从头部向后遍历(next)
            for (int i = 0; i <= index; i++)
                e = e. next;
        } else {
            // 如果index距离list中间位置较远,则从头部向前遍历(previous)
            for (int i = size; i > index; i--)
                e = e. previous;
        }
    return e;
}

add(E e)

public boolean add(E e) {
    addBefore(e, header);
    return true;
}

// 该方法调用addBefore方法,然后直接返回true,
// 对于addBefore(),它为LinkedList的私有方法。

private Entry addBefore(E e, Entry entry) {
    //利用Entry构造函数构建一个新节点 newEntry,
    Entry newEntry = new Entry(e, entry, entry.previous);
    //修改newEntry的前后节点的引用,确保其链表的引用关系是正确的
    newEntry.previous.next = newEntry;
    newEntry.next.previous = newEntry;
    //容量+1
    size++;
    //修改次数+1
    modCount++;
    return newEntry;
}

LinkedList就是这样一个节点一个节点关联起来的,我们可以看一下下面的图片


remove(int index)

/**
 * Removes the element at the specified position in this list.  Shifts any
 * subsequent elements to the left (subtracts one from their indices).
 * Returns the element that was removed from the list.
 *
 * @param index the index of the element to be removed
 * @return the element previously at the specified position
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E remove(int index) {
    //检查index是否超出范围size
    checkElementIndex(index);
    //删除指定位置的节点,首先得找到这个节点
    return unlink(node(index));
}
//检查index是否正确
private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}


/**
 * Returns the (non-null) Node at the specified element index.
 */
//返回指定index位置的节点
Node node(int index) {
    // assert isElementIndex(index);
    
    //首先去比较index和size >> 1(也就是size的一半),
    //如果比中间数小则从链表头找,否则从尾找
    if (index < (size >> 1)) {
        Node x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

可以看到,我们想要移除指定位置的节点,首先得找到这个节点(这个是重点),关键我们的链表如何找到指定位置?不知道大家有没有自己比较好的方法,我们来看下jdk的思路(我觉得jdk的实现方法很有趣),node(int index)方法中的if条件拿index和size的一半比较,文字解释太麻烦了,我来画个图吧:


假设:我们linkedList的长度为6,如要remove的index为3,此时我们3 < (size >> 1)为false,则我们从linkedList后面开始直到变量i>index,此时i变量位置的就是index位置的node,如果我们index是2,那么是从前面开始的,我们大家想这样其实是把linkedList从中间分为2个,速度是不是比没分之前快两倍。

LinkedList 应用场景

排序、频繁增删

你可能感兴趣的:(Java 链表 LinkedList)