列表和队列

总结在前

ArrayList内部采用动态数组实现。非线程安全。

  1. 可以随机访问——通过索引位置访问,效率高;时间复杂度为O(1), 与长度无关;
  2. 除非已排序,否则,按内容查找效率比较低,复杂度为O(N);
  3. 添加元素会进行数组复制,可能还会重新分配内存长度,与添加的元素个数正相关,复杂度为O(N);
  4. 插入和删除元素效率比较低,同样会移动元素(也是数组赋值),复杂度为O(N);

LinkedList内部采用双向链表实现。

  1. 按需分配空间,维护了长度,首尾节点,但是没有对长度做限制
  2. 不可以随机访问,按索引访问效率比较低,必须从头或尾进行遍历,效率为O(N/2);
  3. 不论是否已排序,按照内容查找元素效率低,O(N);
  4. 在两端添加、删除元素效率很高,为O(1);
  5. 在中间插入、删除元素,要先定位(即查找),效率为O(N);但是插入和删除本身效率很高,为O(1);

ArrayDeque内部采用循环数组实现。

  1. 在首尾添加、删除元素的效率很高,但是动态扩展需要内存分配以及数组复制,整体效率为O(N);
  2. 根据元素内容查找和删除的效率比较低,为O(N);
  3. 没有索引位置的概念,不能根据索引位置操作

ArrayList和LinkedList都实现了List接口
如果列表长度未知,添加、删除操作比较多,尤其经常从两端进行操作,而按照索引位置访问相对比较少,LinkedList比较理想。
对于列表长度已知,只添加、不插入和删除的场景ArrayList足以应对,尤其是ArrayList支持随机访问。

LinkedList和ArrayDeque都实现了双端队列Deque接口
如果只需要Deque接口,从两端进行操作,ArrayDeque效率要高于LinkedList。
但如果还要根据索引位置操作,或经常需要在中间进行插入、删除,则应该选择LinkedList。

ArrayList

基本实现

ArrayList是一个泛型容器,可以理解为动态数组。主要方法有添加元素、查询元素下标、通过下标查询元素、移除元素等。
其实现类似于StringBuilder,也是以一个数组存储元素,只不过这个数组并不是特定类型的元素,而是Object[]
类似StringBuilder,ArrayList也有一个默认数组长度为10,且每次添加元素都要判断数组长度是否足以容纳要加入的元素,如果长度不足需要扩充数组长度。(ArrayList中是进行1.5倍长度增长,StringBuilder中是进行2倍长度增长,基本思路一致
下面代码段摘录了成员变量和add、remove元素方法。

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
    private static final int DEFAULT_CAPACITY = 10; // 默认长度为10;
    private static final Object[] EMPTY_ELEMENTDATA = {};
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //默认空数组数据
    transient Object[] elementData; // 存储数据的结构
    private int size; // 数组中真实保存的元素个数,初始态是0,且从构造方法看,没有默认0值填充
    protected transient int modCount = 0; // 数组被修改的次数———增、删都会引起该值增1,变动了数组结构。修改元素值不会自增
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
        }
    }

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e; //加入到元素最后
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); //最小数组长度为DEFAULT_CAPACITY=10
    }
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++; //数组元素修改次数增1
        if (minCapacity - elementData.length > 0) //先减法再判断,防止越限。
            grow(minCapacity);
    }
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);  //目标数组长度增大到1.5倍
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity; //取
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity); //数组扩充到newCapacity长度,由elementData填充前0~size位置的元素
    }
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
    public E remove(int index) {
        rangeCheck(index); // 判断index是否越限,如果超过size的值,表示超出元素数抛出异常
        modCount++;
        E oldValue = elementData(index);
        int numMoved = size - index - 1; //要移动的元素个数
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
            //将elementData的numMoved个元素从index+1开始复制到从index开始的位置,然后将最后一个元素置为null,使GCRoots不可达
        elementData[--size] = null; // clear to let GC do its work
        return oldValue;
    }
}

迭代

对于容器而言,可以和数组类似使用其下标值进行迭代,也可以使用foreach进行迭代,其编译为class文件时,实际上是iterator迭代。

ForEach迭代的本质

.java代码块:
        ArrayList<Integer> intList = new ArrayList();
        intList.add(5); intList.add(50);
        for (Integer integer : intList) {
            System.out.println(integer);
        }
.class代码块:
        ArrayList<Integer> intList = new ArrayList();
        intList.add(5);
        intList.add(50);
        Iterator var7 = intList.iterator();

        while(var7.hasNext()) {
            Integer integer = (Integer)var7.next();
            System.out.println(integer);
        }

迭代器接口

具备forEach迭代能力的类都实现了Iterable接口,该接口返回一个Iterator迭代器对象,通过该迭代器可以进行从首元素开始进行后向元素判断和遍历(hasNext和next),并且提供了安全的迭代内元素移除方法remove;而ListIterator接口继承了Iterator,对其进行了增强,可以从任意位置开始迭代,并且提供了前向元素判断和遍历能力(hasPrevious和previous),并且增强了添加add和修改set方法。

迭代的陷阱
在迭代中,由于会维护一些索引位置相关的数据,要求在迭代过程中,容器不能有结构性变化(元素个数变化),否则索引位置就失效了,即不可以有add、remove、insert操作。
但是可以通过迭代器遍历,进行结构性调整。
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

看一下ArrayList的实现。它有一个内部类Itr implements Iterator,并且有一个内部类ListItr implements ListIterator。

    private class Itr implements Iterator<E> {
        int cursor;       // 下一个要放回的元素位置,迭代游标,从0开始
        int lastRet = -1; // 上一个被返回的元素位置,-1表示没有这个元素
        int expectedModCount = modCount; //期望变更次数,该值必须与modCount一致才能继续迭代

        Itr() {}

        public boolean hasNext() {return cursor != size;}

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
            try {
                ArrayList.this.remove(lastRet); //上一次被返回的位置被移除;调用的是ArrayList的remove(index)方法
                cursor = lastRet;  //游标上一到上次处理的位置
                lastRet = -1; // 
                expectedModCount = modCount;  //这里是迭代器可以进行数组结构变动的关键。在ArrayList的remove方法中,只会增加modCount。
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
        final void checkForComodification() { //如果modCount与期望值不相等则抛出异常。
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

迭代器的优势

  1. forEach语法更加简洁;
  2. 迭代器更加通用,所有的容器都可以实现。这是一种关注点分离的思想,将数据的实际组织方式与数据的迭代遍历分离
  3. 迭代器的性能要好一些:因为提供Iterator接口的代码更了解数据的组织形式,可以提供更高效的实现。

ArrayList实现的其他接口

Collection——表示一个数据集合,数据间没有位置或顺序的概念。方法主要是针对元素的添加、移除;
List——表示有顺序或位置的数据集合,它扩展了Collection,增加了针对数据下标的相关方法。
在这里插入图片描述在这里插入图片描述
RandomAccess——标记接口,表示可以随机访问。在一些通用算法代码中,通过判断该标记接口选取高效的实现。如Collections.binarySearch方法,就通过判断是否实现了RandomAccess选择合适的二分查找算法。

    public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }

ArrayList的特点

其内部采用动态数组实现。非线程安全。

  1. 可以随机访问——通过索引位置访问,效率高;时间复杂度为O(1), 与长度无关;
  2. 除非已排序,否则,按内容查找效率比较低,复杂度为O(N);
  3. 添加元素会进行数组复制,可能还会重新分配内存长度,与添加的元素个数正相关,复杂度为O(N);
  4. 插入和删除元素效率比较低,同样会移动元素(也是数组赋值),复杂度为O(N);

LinkedList

LinkedList实现了List和Deque接口,其中Deque又继承了Queue接口。
Queue——队列接口,先进先出原则(在尾部添加元素,从头部删除元素),扩展了Collection。

public interface Queue<E> extends Collection<E> {
    boolean add(E e);//向尾部添加元素e
    boolean offer(E e);
    E remove(); //删除头部元素——返回头部元素,并将头部元素删除
    E poll();
    E element();//查看头部元素——返回头部元素,但不改变队列
    E peek();
}

Java中没有专门定义栈接口(先进后出),而是通过双端队列Deque实现的。Deque继承了Queue,并提供了操作头部元素的方法:push、pop、peek(与Queue一致):

  • push:在头部添加元素,栈的空间是有限的,沾满了就会抛出异常IllegalStateException;
  • pop:返回头部元素,并且将其删除,如果栈为空,则抛出异常NoSuchElementException;
  • peek:查看头部元素,栈为空时返回null。
    同时Deque由于是双端队列,还提供了offer、add、poll、remove等都提供了xxxFirst用来操作头部元素、xxxLast操作尾部元素。

因此,LinkedList可以用作队列和栈。

       LinkedList<Integer> list = new LinkedList<>();
        Queue<Integer> queue = list; //用作队列
        queue.offer(1);queue.offer(2);queue.offer(3);//尾部添加
        while(queue.peek() != null) {
            System.out.println(queue.poll());//队列用法,出队列——头部,输出:1、2、3
        }
        Deque<Integer> deque = list; //用作双端队列
        deque.push(1);deque.push(2);deque.push(3); //栈的用法,入栈——头部添加
        deque.offer(4);//队列的用法,尾部添加
        while(deque.peek() != null) {
            System.out.println(deque.pop()); //栈的用法,出栈——头部,输出3、2、1、4
        }

LinkedList用作List时与ArrayList基本类似,有add、get、remove、set、indexOf等方法。

实现原理

静态内部类Node用来存储链表中的元素,由于LinkedList实现了Deque双端队列,因此它是一个双向链表,Node中有前向pre、后向next元素索引,并且保存了本元素的值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;
        }
    }

LinkedList中first和last分别记录链表的首尾,size记录链表中元素个数。

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;
    public boolean add(E e) {
        linkLast(e); //将元素e放入link的尾部
        return true;
    }
    void linkLast(E e) {
        final Node<E> l = last; //找到链表的尾元素
        final Node<E> newNode = new Node<>(l, e, null); //创建新的Node,其pre=last,next=null
        last = newNode; //将链表尾元素置为新Node
        if (l == null)
            first = newNode; //如果原来的尾元素是null,说明是一个空list,原first也是null,将first置为与last相等,此时list中只有一个元素
        else
            l.next = newNode;//否则,原尾元素的next由null设置为新的Node
        size++;
        modCount++;//list元素个数和修改次数都加1
    }
    
    public E get(int index) {//根据索引获取元素
        checkElementIndex(index); //检查index是否超出size范围,超出则抛出异常
        return node(index).item;
    }
    Node<E> node(int index) { //无法直接通过index获取到元素,需要前向或后向沿链表遍历
        if (index < (size >> 1)) { //如果索引位置小于size的一般,则前向遍历
            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;
        }
    }

    public int indexOf(Object o) {//通过元素值查找索引位置——LinkedList允许存入null
        int index = 0;
        if (o == null) { //如果要找的元素是null,则查找第一个为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;
    }

    public void add(int index, E element) {//插入到指定位置
        checkPositionIndex(index);
        if (index == size)
            linkLast(element); //如果index就是size表示这是在尾部添加元素,等同于add(E e)
        else
            linkBefore(element, node(index));//将element插入到原node(index)前面,即将element插入到index位置
    }
    void linkBefore(E e, Node<E> succ) {
        final Node<E> pred = succ.prev; //找到succ的前向元素pred
        final Node<E> newNode = new Node<>(pred, e, succ);//新节点:连接pred和succ,元素值为e
        succ.prev = newNode; //succ的prev指向新节点
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;//pred的next指向新节点
        size++;
        modCount++;
    }
    
    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));//将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) {//x就是首元素时
            first = next;
        } else {
            prev.next = next;
            x.prev = null; //一定要将x的前向和后向元素置为null,
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
        x.item = null;
        size--;
        modCount++;
        return element;
    }
}

对于迭代,LinkedList与ArrayList类似,都是通过内部类实现的。

LinkedList的特点

  1. 按需分配空间,维护了长度,首尾节点,但是没有对长度做限制
  2. 不可以随机访问,按索引访问效率比较低,必须从头或尾进行遍历,效率为O(N/2);
  3. 不论是否已排序,按照内容查找元素效率低,O(N);
  4. 在两端添加、删除元素效率很高,为O(1);
  5. 在中间插入、删除元素,要先定位(即查找),效率为O(N);但是插入和删除本身效率很高,为O(1);

如果列表长度未知,添加、删除操作比较多,尤其经常从两端进行操作,而按照索引位置访问相对比较少,LinkedList比较理想。
对于列表长度已知,只添加、不插入和删除的场景ArrayList足以应对,尤其是ArrayList支持随机访问。

ArrayDeque

双端队列实现类,通过循环数组实现。

实现原理

    transient Object[] elements; // non-private to simplify nested class access
    transient int head;
    transient int tail;
    private static final int MIN_INITIAL_CAPACITY = 8;
        public ArrayDeque() {
        elements = new Object[16];
    }
    public ArrayDeque(int numElements) {
        allocateElements(numElements);
    }

head与tail相同,则数组为空,数组长度为0;
tail大于head,则数组从head开始至tail-1结束;
tail小于head且tail=0,则数组从head开始至element.length-1结束。
tail小于head且tail>0,则数组从head开始至element.length-1,然后从0至tail-1结束。

elements的长度最小为8;默认16;且真实长度是比numElements大的2的整数次幂的最小值。(如numElements=12,真实长度是16)

为何要比numElements大?——循环数组需要至少留一个空位,tail变量指向下一个空位

添加元素:
以最简单的长度为8的数组为例,head=0,tail=7,在添加新元素后tail=8,如果和elements.length-1按位与,则值为0,与head相等。据此判断需要扩展数组容量。

    public boolean add(E e) {
        addLast(e);//默认向尾部添加元素
        return true;
    }
    public void addLast(E e) {
        if (e == null)//不允许向ArrayDeque中添加null,
            throw new NullPointerException();
        elements[tail] = e; //tail位置存储新元素
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
            doubleCapacity(); //将数组容量扩大一倍,
    }
    private void doubleCapacity() {
        assert head == tail;//确认数组已满
        int p = head; 
        int n = elements.length;
        int r = n - p; // number of elements to the right of p
        int newCapacity = n << 1; //数组长度翻倍
        if (newCapacity < 0)
            throw new IllegalStateException("Sorry, deque too big");
        Object[] a = new Object[newCapacity];
        System.arraycopy(elements, p, a, 0, r);//将elements中p开始的r个元素,复制到a的0开始的位置
        System.arraycopy(elements, 0, a, r, p);//将elements中0开始的p个元素,复制到a的r开始的位置
        elements = a;
        head = 0;
        tail = n; 
    }

头部添加
举例:head=tail=0时,如果length=8;addFirst后,1111&0111=7,因此head=7。

    public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[head = (head - 1) & (elements.length - 1)] = e;
        if (head == tail)
            doubleCapacity();
    }

出队列:

    public E poll() {
        return pollFirst();//默认头部出队列,先入先出
    }
    public E pollFirst() {
        int h = head;
        E result = (E) elements[h];//找到要出队列的元素
        if (result == null)
            return null;
        elements[h] = null;     // Must null out slot
        head = (h + 1) & (elements.length - 1);  //计算新的头部——后移一位,注意大量的位与运算
        return result;
    }

    public E pollLast() {
        int t = (tail - 1) & (elements.length - 1);//计算要出队列的位置,tail指向的是null的位置,tail-1才是队列尾
        E result = (E) elements[t];
        if (result == null)
            return null;
        elements[t] = null;
        tail = t;
        return result;
    }

查看数组中元素长度:
注意在循环数组中,存在大量和(elements.length-1)进行的位与运算。

    public int size() {
        return (tail - head) & (elements.length - 1);
    }

ArrayDeque的特点

  1. 在首尾添加、删除元素的效率很高,但是动态扩展需要内存分配以及数组复制,整体效率为O(N);
  2. 根据元素内容查找和删除的效率比较低,为O(N);
  3. 没有索引位置的概念,不能根据索引位置操作
    如果只需要Deque接口,从两端进行操作,ArrayDeque效率要高于LinkedList。
    但如果还要根据索引位置操作,或经常需要在中间进行插入、删除,则应该选择LinkedList。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值