美文网首页
ArrayList和LinkedList数据结构对比分析

ArrayList和LinkedList数据结构对比分析

作者: 折青梅 | 来源:发表于2021-03-04 17:45 被阅读0次

ArrayList

ArrayList内部是一个elementData的数组,该数组的默认初始化是EMPTY_ELEMENTDATA或DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空数组。

transient Object[] elementData;
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //初始化大小传小于0抛异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

可以看出,在初始化指定或不指定是,ArrayList的大小size=0

add(E e)

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
 private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
      //第一次minCapacity=1,DEFAULT_CAPACITY=10
        ensureExplicitCapacity(minCapacity);
    }
/**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

ArrayList在第一次执行add的时候,会判断size + 1和DEFAULT_CAPACITY(10)的大小,所以,在第一次add的时候就进行了一次扩容

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
            // 第一次oldCapacity 为0
        int oldCapacity = elementData.length;
            //扩容大小=当前大小+当前大小/2(>>1),为1.5倍扩容
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            //第一次扩容大小为0
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            //数组超过了MAX_ARRAY_SIZE 后,大小位Integer.MAX_VALUE
            newCapacity = hugeCapacity(minCapacity);
            //调用Arrays.copyOf,赋值一个新的数组
            elementData = Arrays.copyOf(elementData, newCapacity);
    }

所以在执行add的时候,如果数组大小<10就会进行扩容,所以当我们传入>=10的初始化大小即调用ArrayList(int initialCapacity),传入>10的时候第一次将不会扩容,这里在第一次add的时候会节省性能。

add(E e)和add(int index, E element)对比

 public boolean add(E e) {
        ensureCapacityInternal(size + 1); 
        //最后一个赋值
        elementData[size++] = e;
        return true;
    }
public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        ensureCapacityInternal(size + 1);  
        //从当前index位置开始copy(size - index为数组index后的长度)当前数组数据并复制到index + 1
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //当前位置index赋值
        elementData[index] = element;
        size++;
    }

由此可见,add index方法有一个arraycopy和数组挪移的过程,所以相对来说,插入效率较低,
同理,remove(int index)也是将数组前移,来达到删除的效果

remove(int index)

public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        modCount++;
        E oldValue = (E) elementData[index];

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //数组前移过后将最后一个位置赋值为null,并size-1来准确大小
        elementData[--size] = null; // clear to let GC do its work
        return oldValue;
    }

数组的减容

ArrayList有扩容机制,每次扩容大小为数组大小的1.5倍,当数组的大小非常大时,那么扩容的大小就会很大,所以ArrayList提供了减容的函数,当我们已经不在为ArrayList添加数据的时候,可以调用trimToSize函数,来为ArrayList瘦身。

public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

所以数组的插入和删除有一个arraycopy的过程。
总结:
1.初始化ArrayList大小的时候,传入大于10的大小,可以在第一次add的时候防止扩容,可以提高插入数据的效率
2.数组每次扩容大小为1.5倍
3.ArrayList在插入和删除的时候,都会进行arraycopy的数组的挪移操作。
4.不再向数组进行插入操作时,应当进行trimToSize来为数组瘦身
5.ArrayList允许插入空值,并且会执行size++操作
6.ArrayList是线程不安全的,可以看到内部有modCount字段,这个字段,在序列化(writeObject)的时候会比较值,如果序列化前的modCount!=序列化后的modCount将抛出ConcurrentModificationException异常,这个异常是并发的异常,如果需要ArrayList这种数据结构做并发的话,有Vector,他是为每个方法加上synchronized内置锁来处理并发的,效率很低。

ArrayList插入和删除的过程

![删除.png](https://img.haomeiwen.com/i4972214/448dac41c7f8c294.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

删除的位置赋值了原数组后一位的数据,原数组最后一位被赋值了null,当gc的时候将被回收。

LinkedList

LinkedList的成员变量有Node<E> first和Node<E> last,Node.class是LinkedList内部私有静态类,Node成员变量有item,next,prev

//指向第一个节点
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;
        }
    }

可以看出,每个node节点,都存储了上一个节点,当前节点,和下一个节点,所以LinkedList实际上是一个双向链接的数据结构。
上一个节点指向上一个元素的下一个节点,下一个节点指向下一个元素的上一个节点,这就是双向链表,可能有点绕,看下图

如图: LinkedList.png
按照惯例,看看add方法
public boolean add(E e) {
        linkLast(e);
        return true;
    }

void linkLast(E e) {
        //定义一个变量l,赋值为last,第一次add的时候last为null
        final Node<E> l = last;
        //new一个新的节点,传入l,e,null分别表示上一个节点,当前元素和下一个节点,所以在第一次add的时候,他的上一个节点            
        //和下一个节点指向均为null
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            //第一次add的时候,newNode作为链表的fist元素,目前他的上下节点都为null
            first = newNode;
        else
            //下一次的add,则将上一个元素的next节点,赋值为新的newNode元素,而newNode的上一个节点l,就是上一个元素的          
            //next节点,这里有点绕,结合new Node<>(l, e, null)来细品
            l.next = newNode;
        size++;
        modCount++;
    }

所以,LinkedList的插入就是移动节点的指向,我们再看add(int index, E element)

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 linkLast(E e) {
        //定义变量l并赋值为最后一个节点
        final Node<E> l = last;
        //创建一个node节点,并且他的上一个节点为last节点
        final Node<E> newNode = new Node<>(l, e, null);
        //再将当前的last节点赋值为newNode节点,更新last节点
        last = newNode;
        if (l == null)
            //如果是第一次插入,赋值first 为newNode节点,相当于创建了一个first元素,他的下一个节点为null,上一个节点也为null
            first = newNode;
        else
            //如果不是,就将l节点(现在的last的上一个节点,也就是newNode的pre节点)的next节点赋值newNode
            l.next = newNode;
        //大小+1
        size++;
        //操作次数+1
        modCount++;
    }
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++;
    }

linkBefore函数同理,但是linkBefore在创建时,寻找节点的位置调用了node(int index)函数

Node<E> node(int index) {
        if (index < (size >> 1)) {
            //如果index是小于链表长度的一半定义x变量为first元素
            Node<E> x = first;
           // 从first元素开始,遍历index次,将所有i位置的元素的item赋值为他的下一个节点,来达到元素节点的查找
            for (int i = 0; i < index; i++)
              //找到当前index位置的节点的元素
                x = x.next;
            return x;
        } else {
            //同理,这里从后面来遍历
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
如图 节点查找.png

接下来,就是找到了index位置的元素,就是插入当前的元素了

void linkBefore(E e, Node<E> succ) {
        //获取当前index那个元素的上一个节点
        final Node<E> pred = succ.prev;
        //创建一个新的元素,上一个节点就是index元素的上一个节点,下一个节点就是index元素
        final Node<E> newNode = new Node<>(pred, e, succ);
        //改变index元素节点上一个节点,指向newNode
        succ.prev = newNode;
        if (pred == null)
         //如果插入到头
            first = newNode;
        else
          //插入到之中,赋值下一个节点
            pred.next = newNode;
        size++;
        modCount++;
    }

完整的插入动作


完整的插入过程.png

简单点来说,就是插入创建newNode的过程,定义prev和next时候,赋值了原来index位置的上下节点,也就是移动指针


插入过程.png
再来看get函数

get(int index)

 public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

可见,get函数也是通过一个for循环,去循环遍历,拿到index位置的节点的item
再看删除:

 public E remove(int index) {
        checkElementIndex(index);
        //进行node遍历查找
        return unlink(node(index));
    }
E unlink(Node<E> x) {
        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;
    }

删除也大同小异,也就是挪移指针,将index的item=null
总结
1.LinkedList是一个双向链表,内部维护了Node节点,分别指向prev上一个节点和item当前节点,next下一个节点
2.LinkedList就插入(add(index)),删除(remove(index)),查找(get(index))都要进行循环遍历节点,去获取index位置的节点,这里采用的node函数判断了长度的一半,进行前后遍历才节省遍历时间

对比:
ArrayList:就插入,和删除,都要进行数组的System.arrayCopy操作,进行数组的挪移,插入还要校验扩容
所以相比于LinkedList,ArrayList的插入和删除效率较低,查找是根据下边查找,get(int index)
LinkedList:插入和删除只是遍历节点,改变pre,next的指向,遍历又有长度/2的优化,所有相比于ArrayList,LinkedList的插入和删除效率较高,但是查找也要遍历节点,查找性能较差。
注意:性能上只是相对,当ArrayList不进行扩容操作时,数据量又特别大时候,,链表的一半长度,和数组插入到最后几个位置,挪移的速度肯定远大于遍历/2长度的速度。


我的刀呢.jpg

相关文章

网友评论

      本文标题:ArrayList和LinkedList数据结构对比分析

      本文链接:https://www.haomeiwen.com/subject/jmzhqltx.html