Java容器源码分析之LinkedList到底是怎样的一个链表

上一篇分析了ArrayList的源码部分,基本了解了ArrayList实际是对数组的增删改查进行了包装,而且支持动态扩容。但是ArrayList和数组一样,同样存在查找快,增删慢的特点。

有没有容器更适合用来插入和删除的元素呢,链表就是其中一种适合增删的数据结构,而LinkedList就是链表结构应用之一。

在研读源码之前,先来了解链表这种数据结构,常见的链表有单向链表,双向链表,循环链表,链表的结构包括:头节点,尾节点,和维护节点关联关系的指针。和数组相比:

  • 存储空间:数组对于内存的要求比较高,需要一块连续的内存空间来存储,如果你申请的一个100M大小的数组,当内存中没有连续的,足够大的存储空间时,即便剩余内存可用空间大于100MB,申请仍然会是失败的。而链表是一种松散的数据结构,对内存没有连续性要求,节点是通过指针关联的;
  • 性能:数组特点查询快,增删慢,查询是根据数据下标,增删可能涉及耗时的数据搬移;链表是查询慢,增删快,查询需要从头来遍历链表,增删之需要修改节点之间的指针关系;

继承体系:

LinkedList继承了抽象类AbstractSequentialList,实现了基本的容器操作方法,实现List接口,Deque接口,Cloneable接口,java.io.Serializable支持序列化

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;//当前链表尾节点,尾节点的特点:前驱指针指向上一个节点,后继指针为null

//transient关键字标记的成员变量不参与序列化过程。

构造方法:相比于ArrayList,LinkedList只有两个构造方法,一个无参的构造方法,另一个是包含集合collection元素的列表。

//无参构造方法
public LinkedList() {
    }
//包含指定collection元素的列表
public LinkedList(Collection<? extends E> c) {
        this();
       //addAll方法先会把Collection转化成数组
        addAll(c);
    }

在LinkedList中有一个重要的内部类也就是节点类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;
        }
    }

重点方法分析:

 在链表尾部添加元素方法:add(E e);

在链表的尾部添加元素,分两种情况,当尾节点不为空时,说明链表中已存在数据,只需要在将当前链表的尾节点作为新节点的前驱指针,尾节点的后继指针指向新节点,新增节点作为尾节点;尾节点为空,也就是说,链表为空时,新加入的节点即使首节点也是尾节点。

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    //如果末尾节点为空,则新增元素为尾节点,否则将原尾节点的下一个元素节点指向新增元素
    //尾节点的特点:后继指针为null
    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
            l.next = newNode;
        size++;//大小增加1
        modCount++;//修改次数增加1
    }

在指定位置添加元素:add(int index, E element)

首先校验插入的位置是否在链表中;判断插入的位置,如果插入位置在链表的末尾,则在链表末尾追加元素;否则通过二分查找先从首节点开始遍历,如果从未找到元素,则从尾节点开始遍历找倒序查找;将新节点追加到指定位置之前,修改指针指向。

    public void add(int index, E element) {
        //校验下标的合法性
        checkPositionIndex(index);
        //如果插入位置等于链表大小,那么就在文章末尾插入元素
        if (index == size)
            linkLast(element);
        else
            //在中间节点位置插入元素
            linkBefore(element, node(index));
    }
      //找到指定位置的节点,从这段代码中可以看出,找到指定位置的节点元素,
      //是要循环遍历这个链表,不过这里已经采用了二分查找优化了查找的算法
      Node<E> node(int 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;
        }
    }
    //插入新元素的方法
    void linkBefore(E e, Node<E> succ) {
        //获取旧元素的前驱节点
        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++;//链表大小加1
        modCount++;//修改次数+1
    }

获取指定位置的元素:get(int index),校验索引是否越界,通过二分查找遍历链表

    public E get(int index) {
        //校验索引是否越界
        checkElementIndex(index);
        return node(index).item;
    }
     //找到指定位置的节点,从这段代码中可以看出,找到指定位置的节点元素,
     //是要循环遍历这个链表,不过这里已经采用了二分查找优化了查找的算法    
     Node<E> node(int 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;
        }
    }

修改固定位置的元素:set(int index, E element)  节点位置不变,只修改元素的值。

    public E set(int index, E element) {
        //校验索引合法性
        checkElementIndex(index);
        //获取指定位置的元素(同一个方法,不在赘述)
        Node<E> x = node(index);
        //获取旧的元素值
        E oldVal = x.item;
        //将新的元素值赋值给节点信息
        x.item = element;
        return oldVal;
    }

 清除链表:clear(),循环遍历,直到每个节点的关联关系都被打破。

//编列所有元素,将对于的节点信息置为null,大小修改为0   
 public void clear() {
        for (Node<E> x = first; x != null; ) {
            Node<E> next = x.next;
            x.item = null;
            x.next = null;
            x.prev = null;
            x = next;
        }
        first = last = null;
        size = 0;
        modCount++;
    }

默认删除的是首节点,返回删除元素:remove(),默认移除首节点的关联关系

    public E remove() {
        return removeFirst();
    }
    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
    //移除首节点,将首节点的下驱节点置为首节点
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

还有队列的的方法push(E e)和pop(),其实就是对首节点和尾节点的操纵,先进先出

    public void push(E e) {
        addFirst(e);
    }
    public void addFirst(E e) {
        linkFirst(e);
    }
    private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }

    public E pop() {
        return removeFirst();
    }
    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

总结看,LinkedList是一个更适合增删元素的容器,查找元素从需要遍历元素,采用二分查询算法,最坏情况时间复制度是O(logn),新删元素的时间复制度为O(1)。

备注:JDK1.6之前为循环链表,JDK1.7以后取消循环,源码展示为JDK版本:1.8.0_241

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值