• LinkedList集合(JDK1.8)


    简述

     按照上篇笔记ArrayList集合继续进行介绍list的另一个常见子类LinkedList

    ?LinkedList介绍

    1.数据结构

    说明:linkedlist的底层数据结构是个双向链表结构,也意味着linkedlist在进行查询时效率会比ArrayList的慢,而插入和删除只是对指针进行移动,相对于ArrayList就会快很多

    2.源码分析

    2.1类的继承关系

    public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable

    2.2类的属性

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

    说明:linkedlist的结构中,一个头结点,一个尾节点,一个表示链表中实际元素个数的变量。注意,头结点、尾结点都有transient关键字修饰,这也意味着在序列化时该域是不会序列化的。

    2.3构造函数

    public LinkedList() {
    }
    
    public LinkedList(Collection<? extends E> c) {
            // 调用无参构造函数
            this();
            // 添加集合中所有的元素
            addAll(c);
        }

    说明:会调用无参构造函数,并且会把集合中所有的元素添加到LinkedList中。

    2.4核心函数

    1.add函数

     public boolean add(E e) {
            // 添加到末尾
            linkLast(e);
            return true;
        }
    
    void linkLast(E e) {
            // 保存尾结点,l为final类型,不可更改
            final Node<E> l = last;
            // 新生成结点的前驱为l,后继为null
            final Node<E> newNode = new Node<>(l, e, null);
            // 重新赋值尾结点
            last = newNode;    
            if (l == null) // 尾结点为空
                first = newNode; // 赋值头结点
            else // 尾结点不为空
                l.next = newNode; // 尾结点的后继为新生成的结点
            // 大小加1    
            size++;
            // 结构性修改加1
            modCount++;
        }

    举个栗子

    说明:初始化状态效果

     

    说明:linkedlist允许传入值是重复的,并且也允许为null

     2.add(int index, E element)函数

    // 插入元素
    public void add(int index, E element) {
        checkPositionIndex(index);  // 检查是否越界
        if (index == size)          // 在链表末尾添加
            linkLast(element);
        else                        // 在链表中间添加
            linkBefore(element, node(index));
    }
    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++; modCount++; }

    说明:通过先判断index的合法性,然后再与size进行比较,如果等于size的话,就相当于直接调用了addlast方法,若不是,则进行中间插入操作。

    3.addAll函数

    public boolean addAll(Collection<? extends E> c) {
            return addAll(size, c);
        }
    
    // 添加一个集合
    public boolean addAll(int index, Collection<? extends E> c) {
            // 检查插入的的位置是否合法
            checkPositionIndex(index);
            // 将集合转化为数组
            Object[] a = c.toArray();
            // 保存集合大小
            int numNew = a.length;
            if (numNew == 0) // 集合为空,直接返回
                return false;
    
            Node<E> pred, succ; // 前驱,后继
            if (index == size) { // 如果插入位置为链表末尾,则后继为null,前驱为尾结点
                succ = null;
                pred = last;
            } else { // 插入位置为其他某个位置
                succ = node(index); // 寻找到该结点
                pred = succ.prev; // 保存该结点的前驱
            }
    
            for (Object o : a) { // 遍历数组
                @SuppressWarnings("unchecked") E e = (E) o; // 向下转型
                // 生成新结点
                Node<E> newNode = new Node<>(pred, e, null);
                if (pred == null) // 表示在第一个元素之前插入(索引为0的结点)
                    first = newNode;
                else
                    pred.next = newNode;
                pred = newNode;
            }
    
            if (succ == null) { // 表示在最后一个元素之后插入
                last = pred;
            } else {
                pred.next = succ;
                succ.prev = pred;
            }
            // 修改实际元素个数
            size += numNew;
            // 结构性修改加1
            modCount++;
            return true;
        }

    说明:addAll有两个重载函数,addAll(Collection<? extends E>)型和addAll(int, Collection<? extends E>)型,我们平时习惯调用的addAll(Collection<? extends E>)型会转化为addAll(int, Collection<? extends E>)型。

             参数中的index表示在索引下标为index的结点(实际上是第index + 1个结点)的前面插入。在addAll函数中,addAll函数中还会调用到node函数,get函数也会调用到node函数,此函数是根据索引下标找到该结点并返回,具体代码如下

    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; // 返回该结点
            }
        }

    说明:在根据索引查找结点时,会有一个小优化,结点在前半段则从头开始遍历,在后半段则从尾开始遍历,这样就保证了只需要遍历最多一半结点就可以找到指定索引的结点。

    4.indexOf函数

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

    说明:indexOf操作非常简单,就是从头开始遍历整个链表,如果没有就反-1,有就返回当前下标

    举个栗子

    5.remove函数

     public E remove() {
            return removeFirst();
        }
    
     public E remove(int index) {
            checkElementIndex(index);
            return unlink(node(index));
        }
    
    public E removeFirst() {
            final Node<E> f = first;
            if (f == null)   
                throw new NoSuchElementException();
            return unlinkFirst(f);
        }
    
     E unlink(Node<E> x) {
            // 保存结点的元素
            final E element = x.item;
            // 保存x的后继
            final Node<E> next = x.next;
            // 保存x的前驱
            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--; 
            // 结构性修改加1
            modCount++;
            // 返回结点的旧元素
            return element;
        }
    
    private E unlinkFirst(Node<E> f) {
            // assert f == first && f != null;
            final E element = f.item;   //获取删除节点的元素值(f是头结点)
            final Node<E> next = f.next;  //存储要删除节点指向的下一个节点地址
            f.item = null;
            f.next = null; // help GC   //将要删除节点的指针以及值全部设置为null,等待                  垃圾回收
            first = next;    //将头结点向下移动
            if (next == null)
                last = null;       //如果要删除节点的下一个为null,则当前链表只有一个节点存在
            else                   //如果不为null,则将前驱设置为null
                next.prev = null;
            size--;
            modCount++;
            return element;
        }
    View Code

    说明:如果直接调无参的remove(),就会默认删除头节点删除头节点非常简单,就是把头节点的值清空,next清空然后把nextNode只为头节点,然后清空next的prev最后size减1如果是删除中间节点,调用remove(int index)首先判断Index对应的节点是否为头节点,即index是否为0如果不是中间节点,就是x的prev指向x的next

    举个栗子

     

    说明:当LinkedList集合在一边遍历一边进行remove操作时,且当集合元素个数大于2个时,则会发生如下错误:

     

    小结:ArrayList和LinkedList有什么区别?

    • ArrayList查询快是因为底层是由数组实现,通过下标定位数据快。写数据慢是因为复制数组耗时。LinkedList底层是双向链表,查询数据依次遍历慢。写数据只需修改指针引用。
    • ArrayList和LinkedList都不是线程安全的,小并发量的情况下可以使用Vector,若并发量很多,且读多写少可以考虑使用CopyOnWriteArrayList。因为CopyOnWriteArrayList底层使用ReentrantLock锁,比使用synchronized关键字的Vector能更好的处理锁竞争的问题。    

    参考资料:【集合框架】JDK1.8源码分析之LinkedList(七)

                      java集合详解--什么是List

                     Java 常用List集合使用场景分析

  • 相关阅读:
    POJ 3295 spfa判断是否存在负权回路
    hdu 1385 最短路+输出字典序好题
    hdu 1143 递归
    hdu 1598 并查集+贪心其实就是kruskal的思想
    快速排序
    HDU:Fighting for HDU
    HDU:Saving HDU
    HDU:悼念512汶川大地震遇难同胞——老人是真饿了
    每日一题统计指定的字符串
    每日一题2求一个文件里面指定字符出现的次数
  • 原文地址:https://www.cnblogs.com/liangyueyuan/p/10132808.html
Copyright © 2020-2023  润新知