LinkedList源码解读

1.特点:

1.基于链表数据结构(双向链表);

2.下标查询,时间复杂度为log2(n),二分查找;

3.线程不安全;

4.增删改效率高,查询效率比较低;

2.数组与链表结构的区别

数组:保证元素的有序性,可以基于下标精确查找,时间复杂度为O(1),适用于基于下标查询的场景,

链表:单向链表和双向链表,也可以基于下标查找,但是时间复杂度为O(n)=>它需要挨个节点挨个节点的找下去,因此查询效率较低,适用于增删改的场景,因为只需要修改引用的指针关系,而数组删除是把目标元素后的所有元素往前移动一位,这样的效率很低;

3.底层相关参数分析

1.first节点:整个链表里面的头节点(必须要:为了方便后期遍历数据知道从哪里开始

2.last节点:整个链表里面的尾节点(其实可以不用记录);

transient Node<T> first;//没有prev节点的就是头节点;

transient Node<T> last;//没有next节点的就是尾节点;

3.item:节点元素

4.next:当前节点的下一个节点

5.prev:当前节点的上一个节点(从这两个参数就可以看出来LinkedList底层是双向链表而不是单向链表),那么我们就要注意在新增节点的时候需要指定两者之间的关系(需要同时绑定前者的next节点和后者的prev节点才能对应两者的关系

4.add方法

因此在知道相关参数以及它底层是双向链表之后,我们再来思考它的add方法需要做什么操作?

1.创建一个新节点;

2.将新节点的prev节点指向原链表的last节点;

3.新节点的next节点设置为null;

4.链表的尾节点设置为新的这个节点

5.判断原链表的last节点是否为空,如果为空则说明这一次新增操作是当前链表的第一次新增,则将新节点设置为头节点,如果不为空则需要完成两者关系的最后一次绑定即原链表尾节点的next节点指向新节点

然后我们再来看看底层是不是这样实现的:

调用add方法,然后它将参数直接传给了下面的这个linkLast方法;

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;
    else//否则再将原链表尾节点的next节点指向新节点
        l.next = newNode;
    size++;
    modCount++;
}

我们再看在第二步创建新节点的时候调用的有参构造器长什么样子:

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;
        //下一个节点的指向(null)
        this.next = next;
        //上一个节点的指向(原链表的last节点)
        this.prev = prev;
    }
}

底层源码确实也是这样实现的。

5.get方法(折半查找原理)

链表中如果使用普通的根据下标查找的方法,也需要从头节点挨个节点的往下遍历查找,直到找到目标节点,因此时间复杂度为O(n),效率特别低,因此引入了二分查找的算法;

简单理解二分查找:

我们把整个链表的长度除以2取到中间值,然后拿我们传过去的索引值与这个中间值进行对比,根据判断的结果来决定是在前半段查找还是后半段查找,这样就大大的减少了查询的次数,从而提高了查询效率;

例如我们一个链表的长度为10,我们需要查找下标为8的元素,按照之前的方法,我们需要遍历9次才能得到;

而使用二分查找的方法,首先通过10除以二得到中间值5,判断下标值8大于5,则在后半段进行查找,因此从后往前查找,只需要查找3次则得到目标元素,效率大大提高;

源码

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

我们再来分析一下源码的实现逻辑:

1.首先判断传入的索引值与中间值的对比结果;

2.若我们的索引值小于中间值,则说明在前半段进行查找,那么就正常遍历查找到索引前的位置节点,再获取到它的next节点即为目标节点;

3.若我们的索引值大于中间值,说明索引在后半段,那么就从后往前查找,直到遍历到索引前的节点,取到该节点的prev节点即为目标节点

6.remove方法

我们已经知道了linkedList的底层是双向链表结构,因此我们删除就需要改引用:即被删除的节点的prev节点和next节点需要相互引用起来;

源码

无论是根据下标删除还是根据元素值删除,最终它都会获取到具体的节点然后进入下面这个核心方法中:

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

在这个方法中为了避免大家混淆,我们一步一步看,根据if else分成两个板块看;

首先它拿到了我们被删除节点的所有数据:元素值,前节点,后节点;

我们先看第一个if else

1.先对前节点操作

        1.1 判断前节点是否为空,如果为空则说明被删除的节点为头节点,因此我们只需要将被删除节点的next节点设置为头节点即可;

        1.2 若前节点不为空,则正常删除,即被删除节点的prev节点的next节点设置为被删除节点的next节点;

再看第二个if else

2.对后节点操作

        2.1 判断后节点是否为空,如果为空则说明被删除的节点为尾节点,因此我们只需要将被删除节点的prev节点设置为尾节点即可;

        2.2 若后节点不为空,则正常删除,即被删除节点的next节点的prev节点设置为被删除节点的prev节点

3.将被删除节点的元素值和相关prev节点以及next节点设置为null,告诉GC去清理垃圾

4.总数量size-1;

这里可能稍微有一点绕,建议大家画图去理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Strine

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值