zoukankan      html  css  js  c++  java
  • CRUD工程师——基础容器LinkedList

    今天复习到了LinkedList做下笔记记录。
    LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作
    还是这张熟悉的图
    开始对LinkedList进行部分源码分析和功能介绍。
    LinkedList翻译是链表(没错它就是链表)
    进入LinkedList代码首先还是先看最上面的javadoc部分,可以分成五个点:
    1. LinkedList双向链表,实现了List的双向队列接口,实现了所有list可选择性操作,允许存储任何元素(包括null值)
    2. 所有的操作都可以表现为双向性的,遍历的时候会从首部到尾部进行遍历,直到找到最近的元素位置
    3. 注意这个实现不是线程安全的, 如果多个线程并发访问链表,并且至少其中的一个线程修改了链表的结构,那么这个链表必须进行外部加锁。(结构化的操作指的是任何添加或者删除至少一个元素的操作,仅仅对已有元素的值进行修改不是结构化的操作)。
    4. List list = Collections.synchronizedList(new LinkedList(…)),可以用这种链表做同步访问,但是最好在创建的时间就这样做,避免意外的非同步对链表的访问(加锁)
    5. 迭代器返回的iterators 和 listIterator方法会造成fail-fast机制:如果链表在生成迭代器之后被结构化的修改了,除了使用iterator独有的remove方法外,都会抛出并发修改的异常。因此,在面对并发修改的时候,这个迭代器能够快速失败,从而避免非确定性的问题。
    LinkedList的继承体系如图所示。
    很明显的是克隆和序列化接口,这个在ArrayList中已经解释过了。
    Deque接口这个是第一见到,所以点进去看一看。Deque接口中javadoc解释道:
    一个线性集合,支持两端的元素插入和删除。 deque 名称是“双头队列” 的缩写,通常发音为“ deck”。大多数{@code Deque} 实现对元素可能包含的元素数量没有固定的限制,但是此接口支持容量受限的双端队列以及没有固定大小限制的双端队列。
    此接口定义用于访问双端队列两端的元素的方法提供了用于插入,删除和检查元素的方法。这些方法中的每一种都以两种形式存在:一种在操作失败时引发异常,另一种返回特殊值({@code null}或{@code false},具体取决于操作)。插入操作的后一种形式专为容量受限的{{code Deque}实现而设计;在大多数实现中,insert 操作不会失败。
    原来是一个队列,队列是一种线性表,特点是先进先出,那么LinkedList实现了这个接口,也可以代表它其实也是一种队列,也遵守了这个特点。
    也就说它也可以被当做堆栈、队列或双端队列进行使用
    public class LinkedList<E>  
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
    {
    //链表节点的个数
    transient int size = 0;  //将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。
    ////链表首节点
    transient Node<E> first;
    //链表尾节点
    transient Node<E> last;
     
    //无参数构造方法
    public LinkedList() {
    }
     
    //首先会调用无参数的构造方法,然后调用addAll方法将集合内元素全部加入到链表中,addAll方法我们后面会详细的分析。
    从上述的俩个构造方法可以看出LinkedList是一个无界链表,不存在容量不足的问题。
    
    public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
    }
     
    //(1)定义一个Node类型的变量f,指向链表的第一个结点。
    //(2)定义一个新的Node类型的变量newNode,通过Node类的带参数构造函数将插入的元素的值放在变量中,因为头插法,所以新结点的上一个域为null
    //(3)更改第一个结点的值,将newNode的值赋给成员变量first
    //(4)如果f(链表的第一个结点)是null,那么最后一个结点也是newNode,否则将f的prev域指向newNode
    //(5)更改size的大小和modCount的大小
    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++;
    }
     
    //(1) 定义一个Node类型的变量pred,用来指向链表的头结点
    //(2) 定义一个Node类型的变量newNode,用来存储新的将要插入的结点信息
    //(3) 将当前链表的头结点赋值为newNode结点
    //(4) 如果pred为空,即原来链表的头结点为空,表明原链表没有元素,那么刚插入的结点就是链表的第一个结点
    //(5) 最后要修改链表的size大小和modCount的值
    // 这个和上一次linkFirst不同之处在于必须要有succ节点不然会出错,一个不需要succ节点。succ节点是待加入元素的后继节点
    void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    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++;
    modCount++;
    }
     
    //(1) 定义一个E类型的element变量,其值等于链表中第一个结点的值
    //(2) 定义一个Node类型的next变量,其值等于链表中第一个结点的next域
    //(3) 将链表中第一个结点的值赋值为null,并将该结点的next域赋值为null
    //(4) 将next值赋值给first,如果next为空,那么链表中不再有元素,否则将链表中第一个结点删除,即赋值next的prev域为null
    //(5) 更改链表的size值,更改modCount的值
    //(6) 返回链表的第一个结点
    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的工作基本原理了(主要是通过节点进行配合处理,节点提供了前后值包括自己的值),下面看一下增删改查方法。
    查:
     
    get方法中线检查index的范围,检查是否大于0小于size。然后调用node()方法,在node()方法中采用了缩小范围查询的方法,根据index的值和size的大小来决定从后还是从前查。然后根据循环来找。
    改:
    根据传入值,然后建立新的节点x,通过item方法为node建立tiem,next,prev三个值。最后修改item的值。
    增:
    这边有一个优化,如果index是最后,就直接加,不然就使用linkBefore方法,添加新点。因为确定点的位置是通过前值来确定的所以不需要对后续的所有点进行处理,只需要处理前后点即可。
    删:
    传入参数为点,获取到点的前后值之后删除点,并且将前后点关联起来。
    可以看出其实方法并不是特别难以理解。
    也可以看出linkedlist的特点,增删都特别快,但是查找很麻烦。
    linkedlist的常用方法:
    增加:
    add(E e):在链表后添加一个元素;   通用方法
    addFirst(E e):在链表头部插入一个元素;  特有方法
    addLast(E e):在链表尾部添加一个元素;  特有方法
    push(E e):与addFirst方法一致  
    offer(E e):在链表尾部插入一个元素                                                                                                                                         
    add(int index, E element):在指定位置插入一个元素。      
    offerFirst(E e):JDK1.6版本之后,在头部添加; 特有方法                                                                                                        
    offerLast(E e):JDK1.6版本之后,在尾部添加; 特有方法
    删除:
    remove() :移除链表中第一个元素;    通用方法  
    remove(E e):移除指定元素;   通用方法
    removeFirst(E e):删除头,获取元素并删除;  特有方法
    removeLast(E e):删除尾;  特有方法
    pollFirst():删除头;  特有方法
    pollLast():删除尾;  特有方法
    pop():和removeFirst方法一致,删除头。 
    poll():查询并移除第一个元素     特有方法   
    查:
    get(int index):按照下标获取元素;  通用方法
    getFirst():获取第一个元素;  特有方法
    getLast():获取最后一个元素; 特有方法
    peek():获取第一个元素,但是不移除;  特有方法
    peekFirst():获取第一个元素,但是不移除; 
    peekLast():获取最后一个元素,但是不移除;
    pollFirst():查询并删除头;  特有方法
    pollLast():删除尾;  特有方法
    poll():查询并移除第一个元素     特有方法
    可以总结下linkedlist的特点:
         1). LinkedList是通过双向链表去实现的。
         2). 从LinkedList的实现方式中可以看出,它不存在容量不足的问题,因为是链表。
         3). LinkedList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写出“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
         4). LinkdedList的克隆函数,即是将全部元素克隆到一个新的LinkedList中。
         5). 由于LinkedList实现了Deque,而Deque接口定义了在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。
         6). LinkedList可以作为FIFO(先进先出)的队列
         7). LinkedList可以作为LIFO(后进先出)的栈
         8).不是同步的!!!
    和ArrayList的区别:

    1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。 (LinkedList是双向链表,有next也有previous)

    2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。 

    3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

     时间复杂度比较:

    首先一点关键的是,ArrayList的内部实现是基于基础的对象数组的,因此,它使用get方法访问列表中的任意一个元素时(random access),它的速度要比LinkedList快(O1)。LinkedList中的get方法是按照顺序从列表的一端开始检查,直到另外一端(On)。对LinkedList而言,访问列表中的某个指定元素没有更快的方法了

    但在某些情况下LinkedList的表现要优于ArrayList,有些算法在LinkedList中实现时效率更高。比方说,利用Collections.reverse方法对列表进行反转时,其性能就要好些。当要对list进行大量的插入和删除操作时,LinkedList也是一个较好的选择。

    总结 
    ArrayList和LinkedList在性能上各有优缺点,都有各自所适用的地方,总的说来可以描述如下: 
    1.对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。

    2.在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。

    3.LinkedList不支持高效的随机元素访问。

    4.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间

    可以这样说:当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了

  • 相关阅读:
    Tensorflow卷积接口总结
    《MuseGAN: Multi-track Sequential Generative Adversarial Networks for Symbolic Music Generation and Accompaniment》论文阅读笔记
    核函数
    KCF跟踪算法
    岭回归、lasso
    C++的命令行参数(gflag)
    Python的命令行参数(argparse)
    size_t为什么重要
    linux下caffe的命令运行脚本
    卡尔曼滤波
  • 原文地址:https://www.cnblogs.com/SmartCat994/p/12989030.html
Copyright © 2011-2022 走看看