02-LinkedList

LinkedList

一、简述

  • LinkedList是基于链表实现的List集合,内部使用链表保存元素,

二、属性和数据结构

2.1 属性

//头结点
transient Node<E> first;
//尾结点
transient Node<E> last;

2.2 数据结构

  • LinkedList中使用Node来表示链表节点,结构很简单,除了数据域item之外,还包含一个前驱指针和一个后继指针
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;
    }
}    

2.3 构造方法

  • LinkedList的构造方法有重载,但是因为内部属性很少,没有阈值,默认容量等参数,因此默认的构造方法什么都不做。
    public LinkedList() {
    }

三、重要方法

3.1 添加元素

3.1.1 add添加
  • add方法默认在尾部添加;
public boolean add(E e) {
    linkLast(e);
    return true;
}

void linkLast(E e) {
    final Node<E> l = last;
    //1.构造节点,并将新构造的节点设置为last尾节点
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    //2.如果原来的尾节点为null,说明原本的链表是空的
    if (l == null)
        first = newNode;
    //3.原本的链表不为空,就加到尾部即可
    else
        l.next = newNode;
    size++;
    modCount++;
}
3.1.2 头部添加
  • 头部添加
public void addFirst(E e) {
    linkFirst(e);
}
    
private void linkFirst(E e) {
    final Node<E> f = first;
    //1.构造新节点,设为头结点
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    //2.原来的头是null,那么新节点即是头也是尾
    if (f == null)
        last = newNode;
    else
        //3.原来的头不是null,那么原来的头就是新节点的后继
        f.prev = newNode;
    size++;
    modCount++;
}
3.1.3 尾部添加
  • linkLast前面解析过了,和add几乎等价
 public void addLast(E e) {
        linkLast(e);
    }
3.1.4 指定位置添加
  • add方法可以在指定index添加,会先找到index处的目标元素,然后进行链表的指针操作将元素添加进去
    public void add(int index, E element) {
        //1.位置校验,就是判断是否在0和size之间
        checkPositionIndex(index);
        //2.如果是最后的位置,就加在尾部
        if (index == size)
            linkLast(element);
        //3.不是最后的位置,就插在指定元素之前,node方法会定位index的元素
        else
            linkBefore(element, node(index));
    }
  • linkBefore:链接在指定元素前面,修改双向指针即可
void linkBefore(E e, Node<E> succ) {
         
        final Node<E> pred = succ.prev;
        //1.构建新节点,指定前驱后继
        final Node<E> newNode = new Node<>(pred, e, succ);
        //2.succ的前驱指向新节点
        succ.prev = newNode;
        //3.设置新节点的前驱,如果原来的前驱是null,那么自己就是链表头结点
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }
  • 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
            l.next = newNode;
        size++;
        modCount++;
    }

3.2 删除元素

3.2.1 指定位置移除
  • remove删除指定index处的元素,需要先通过node方法定位到元素,然后调用unlink将元素的前驱后继连接起来,移除本身
    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }
3.2.2 头尾移除
  • removeFirst头部移除
    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
3.2.3 尾部移除
    public E removeLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    }
3.2.4 移除指定值
  • 只能暴力查询,然后移除,复杂度O(N)
    public boolean remove(Object o) {
        if (o == null) {
            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;
    }

3.3 修改元素

  • 先定位,再修改,返回旧值
    public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

3.4 查询元素

3.4.1 下标获取
  • 下标获取,获取的过程有点意思,如果在前半部分就从头遍历,如果在后半部分就从尾部遍历,尽量在双向链表中少查找一些元素,不过复杂度依然是O(N)
    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
    
        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;
        }
    }
3.4.2 头部获取
    public E getFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return f.item;
    }
3.4.3 尾部获取
    public E getLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return l.item;
    }
3.4.4 根据值获取
  • 根据值查询,只能暴力查询,然后移除,复杂度O(N)
    public int indexOf(Object o) {
        int index = 0;
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null)
                    return index;
                index++;
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item))
                    return index;
                index++;
            }
        }
        return -1;
    }

3.5 队列操作

  • peek:获取头部元素,但是不移除元素;(element方法和peek的方法作用一样,区别在于没有元素时peek返回null,但是element抛出异常)
    public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }

    public E element() {
        return getFirst();
    }
  • poll:移除头部元素,并返回该元素;(remove方法和poll方法作用一样,区别在于没有元素时poll返回null,但是remove抛出异常)
    public E poll() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }
    
    public E remove() {
        return removeFirst();
    }
  • 另外还有一些队列的方法,比如:remove、offer、offerFirst、offerLast、peekFirst、peekLast、pollFirst、pollLast、push、pop等,就不一一给出了

四、ArrayList和LinkedList对比

4.1 基本对比

对比维度ArrayListLinkedList
数据结构数组链表
空间效率节点没有看见浪费,但是扩容会有空间浪费每个节点需要额外的指针域
优势随机访问头部插入,其他性能相比ArrayList不占有,支持队列Api
默认容量10
扩容1.5倍不需扩容

4.2 性能对比

  • 性能对比可以阅读参考文章[1],这里就直接引用该文的结论
数据量\插入位置头部中间尾部随机
效率持平效率持平效率持平效率持平
LinkedList插入快效率持平效率持平ArrayList插入快
LinkedList插入快ArrayList插入快效率持平ArrayList插入快
十万LinkedList插入快ArrayList插入快ArrayList插入快ArrayList插入快
百万LinkedList插入快ArrayList插入快ArrayList插入快ArrayList插入快
  • 由表格基本上看到,LinkedList只会在头部插入的时候性能占有,此时ArrayList需要进行大量数据的往后拷贝,但是其余的情况都是ArrayList较优;
  • 中间位置的插入,LinkedList需要遍历的元素会跟多,因此更慢,虽然ArrayList需要拷贝,但是性能反而更好。同理尾部也是一样,LinkedList需要遍历的少了,但是ArrayList需要copy的也少了,ArrayList性能反而更好
  • 元素比较多的时候,ArrayList的扩容带来的优势会明显,1.5倍的扩容可以避免频繁扩容
  • 另外存储空间上,ArrayList会浪费一部分扩容后的空间,LinkedList则是每个节点需要额外的两个指针域来保存指针
  • LinkedList的优势在于提供了队列的基本实现

五、小结

  • LinkedList本身的操作,默认的添加操作是在链表的尾部因此比较快,如果不是链表尾部的操作,比如指定index的新增和删除都需要预先根据index找到元素的位置,然后通过指针操作。指针操作是很快的,但是定位的过程会根据位置在前半部还是后半部决定从前开始遍历还是从尾部开始遍历,因此LinkedList的增删操作也不一定"很快";
  • LinkedList的查询操作其实就是前面提到的,判断区域后进行遍历检索,由此看到其实LinkedList的查询效率并不算高,而增删很多时候首先要查询,不能简单地认为LinkedList是链表操作它的增删就会很快,前面的测试结果可以作为参考
  • LinkedList额外提供了队列的方法,这是LinkedList的一个优势,ArrayList是不具备的

六、参考

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值