【Java编程的逻辑】列表和队列

ArrayList

ArrayList中有两个方法可以返回数据

public Object[] toArray();
public <T> T[] toArray(T[] a);

ArrayList中有一个静态方法asList可以返回对应的List

Integer[] a = {1, 2, 3};
List<Integer> list = Arrays.asList(a);

这个方法返回的List,内部就是传入的数组,所以对数据的修改也会反映到List中,对List调用add、remove方法会抛出异常
要使用ArrayList完整的方法,应该新建一个ArrayList

List<Integer> list = new ArrayList<Integer>(Arrays.asList(a));

ArrayList特点总结:
1. 可以随机访问,按照索引位置进行访问效率很高。O(1)
2. 除非数组已排序,否则按照内容查找元素效率比较低。O(N)
3. 插入和删除元素效率比较低。O(N)
4. 非线程安全的

LinkedList

LinkedList还实现了队列接口Queue,队列的特点就是先进先出。

public interface Queue<E> extends Collection<E> {
    boolean add(E e);
    boolean offer(E e);
    E remove();
    E poll();
    E element();
    E peek();
}

Queue主要扩展了Collection接口,主要是三个操作:

  • 在尾部添加元素(add、offer)
  • 查看头部元素(element、peek),返回头部元素,但不改变队列
  • 删除头部元素(remove、poll),返回头部元素,并且从队列中删除
    每个操作都对应了两个方法。它们的区别在于,当队列为空的时候,element和remove会抛出异常,而peek和poll会返回null;当队列为满的时候,add会抛出异常,offer返回false

LinkedList其实是直接实现的Duque接口,该接口表示双端队列。它可以实现栈相关的操作,栈的特点是先进后出,后进先出。

public interface Deque<E> extends Queue<E> { 
    // 表示入栈,在头部添加元素,如果栈满了,会抛出异常
    void push(E e);
    // 出栈,返回头部元素,并且从栈中删除,如果栈为空,抛出异常
    E pop();
    // 查看栈头部元素,如果栈为空,返回null
    E peek();
    // 从后往前遍历
    Iterator<E> descendingIterator();
}

LinkedList和ArrayList用法上类似,只是LinkedList增加了一个接口Deque,可以把它看作队列、栈、双端队列。

LinkedList实现原理

ArrayList内部是数组,元素在内存是连续存放的,但LinkedList不是。LinkedList直译就是链表,它的内部是双向链表,每个元素在内存都是单独存放的,元素之间通过链接连在一起。
为了表示链接关系,需要一个节点,节点包括实际的元素,两个链接,分别指向前一个节点(前驱)和后一个节点(后继)

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内部组成主要由如下三哥实例变量

int size = 0;       // 表示链表长度
Node<E> first;      // 指向头节点
Node<E> last;       // 指向尾节点

添加元素 add

public boolean add(E e) {
    linkLast(e);
    return true;
}

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

与ArrayList不同,LinkedList的内存是按需分配的,添加元素也很简单,直接在尾节点添加链接即可

获取元素 get

public E get(int index) {
    // 检查index是否超出范围
    checkElementIndex(index);
    return node(index).item;
}

Node<E> node(int index) {
    // sieze >> 1相当于size/2
    // 如果index在size的前半部分,则从头节点开始找,否则从尾节点开始
    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;
    }
}

与ArrayList不同,ArrayList中数组元素连续存放,可以根据索引直接定位,而在LinkedList中,则必须从头或尾节点顺着链接查找

插入元素

add是在尾部添加元素,如果在头部或中间插入元素,可以使用如下的方法

public void add(int index, E element) {
    // 检查index是否合法
    checkPositionIndex(index);
    // 如果index正好等于size,那么就直接调用添加方法
    if (index == size)
        linkLast(element);
    else        
        linkBefore(element, node(index));
}
/**
 * @succ 目标位置当前的节点  这里插入后,就变成后继了
 */
void linkBefore(E e, Node<E> succ) {
    // 获取当前位置节点的前驱
    final Node<E> pred = succ.prev;
    // 新建节点,前驱是pred,后继是之前位置上的节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 后驱的前驱重新给值
    succ.prev = newNode;    
    // 前驱的后继重新给值
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    // 增加长度
    size++;
    modCount++;
}

在中间插入元素,LinkedList只需要按需分配内存,修改前驱和后继节点的链接,而ArrayList可能需要分配很多额外的空间,并移动后续元素

remove 删除元素

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

E unlink(Node<E> x) {
    // assert x != null;
    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;
}

删除x节点,基本思路就是让x的前驱和后继链接起来。

LinkedList特点

  1. 按需分配空间,不需要预先分配额外的空间
  2. 不可随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)
  3. 不管列表是否已排序,只要是按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)
  4. 在两端添加、删除元素效率很高,为O(1)
  5. 在中间插入、删除元素,要先定位,效率较低,为O(N),但修改本身的效率很高,为O(1)

如果列表长度未知,添加、删除操作比较多,尤其经常从两端进行操作,而按照索引位置访问相对比较少,则LinkedList是比较理想的选择

ArrayDeque

ArrayDeque是基于数组实现的双端队列,主要有以下几个属性

// 存储队列中节点的数组
transient Object[] elements;
// 代表头指针
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);
}

public ArrayDeque(Collection<? extends E> c) {
    allocateElements(c.size());
    addAll(c);
}

private void allocateElements(int numElements) {
    // 如果numElements小于8,那么数组长度就分配8
    intinitialCapacity = MIN_INITIAL_CAPACITY;
    if (numElements >= initialCapacity) {
        // 如果numElements大于8,分配的长度是严格大于numElements并且为2的整数次幂
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    elements = new Object[initialCapacity];
}

如果没有指定显式传入elements的长度,则默认16。如果显式传入一个代表elements的长度的变量,那么会调用allocateElements做一些简单的处理,主要的处理都在上面的代码注释中。 这里它计算严格大于numElements并且为2的整数次幂的方式,就是先将numElements二进制形式的所有位置1,然后+1就是了。

从尾部添加 add

public boolean add(E e) {
    addLast(e);
    return true;
}

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    // 将元素添加到尾指针的位置    
    elements[tail] = e;
    // 将tail指向向一个位置,如果满了,就扩容
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

这里我们重点看一下判断当前队列是否满了的语句

if ( (tail = (tail + 1) & (elements.length - 1)) == head)

我们之前在构造elements元素的时候,说过它的长度一定是2的指数级,所以对于任意一个2的指数级的值减去1之后它的二进制必然所有位全为1,例如:8-1之后为111,16-1之后1111。而对于tail来说,当tail+1小于等于elements.length - 1,两者与完之后的结果还是tail+1,但是如果tail+1大于elements.length - 1,两者与完之后就为0。就等于头指针的位置了。

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];
    // 将head右边的元素赋值到新数组开头
    System.arraycopy(elements, p, a, 0, r);
    // 将head左边的元素赋值到新数组中
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    // 重新设置head和tail
    head = 0;
    tail = n;
}

从头部添加 addFirst

public void addFirst(E e) {
    if(e == null)
        throw new NullPointerException();
    // 让head指向前一个位置    
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

出栈

public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];
    if (result == null)
        return null;
    elements[h] = null;
    head = (h + 1) & (elements.length - 1);
    return result;
}

该方法很简单,直接获取数组头部元素即可,然后head往后移动一个位置。这是出队操作,其实删除操作也是一种出队,内部还是调用了pollFirst方法。

查看长度

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

ArrayDeque特点

  1. 在两端添加、删除元素的效率很高,动态扩展需要的内存分配以及数组复制开销可以被平摊,具体来说,添加N个元素的效率为O(N)
  2. 根据元素内容查找和删除的效率比较低,为O(N)
  3. 与ArrayList和LinkedList不同,没有索引的概念

迭代器

Iterable Iterator

Iterable 表示可迭代的,它有一个方法iterator(),返回Iterator对象。

public interface Iterable<T> {
    Iterator<T> iterator();
}
public interface Iterator<E> {
    // 判断是否还有元素未访问
    boolean hasNext();
    // 返回下一个元素
    E next();
    // 删除最后返回的元素。如果没有调用过next(),直接调用remove()是会报错的
    void remove();
}

如果对象实现了Iterable,就可以使用foreach语法。foreach语法,编译器会转换为调用Iterable和Iterator接口的方法。

ListIterator

ArrayList还提供了两个返回Iterator接口的方法:

// 返回的迭代器从0开始
public ListIterator<E> listIterator();
// 返回的迭代器从指定位置index开始
public ListIterator<E> listIterator(int index);

ListIterator扩展了Iterator接口

public interface ListIterator<E> extends Iterator<E> {  
    boolean hasPrevious();
    E previous();
    int nextIndex();
    int previousIndex();
    void set(E e);
    void add(E e);
}

迭代器常见问题

有一种常见的误用,就是在迭代的中间调用容器的删除方法:

public void remove(ArrayList<Integer> list) {
    for(Integer a : list) {
        if(a<=100) {
            list.remove(a);
        }
    }
}

这样做是会抛出异常的。因为迭代器内部会维护一些索引位置相关的数据,要求在迭代过程中,容器不能发生结构性变化(添加、删除元素)
使用迭代器的remove方法可以避免错误

public static void remove(ArrayList<Integer> list) {
    Iterator<Integer> it = list.iterator();
    while(it.hasNext()) {
        if(it.next() < 100) {
            it.remove();
        }
    }
}

为什么使用迭代器的remove方法就可以呢?

迭代器基本原理

先看看ArrayList中的iterator方法

public Iterator<E> iterator() {
    return new Itr();
}

Itr是ArrayList的成员内部类,

private class Itr implements Iterator<E> {
    // 下一个要返回的元素的位置
    int cursor;
    // 最后一个返回的索引位置,如果没有为-1
    int lastRet = -1;
    // 期望的修改次数
    int expectedModCount = modCount;

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

    public E next() {
        // 检查是否发生了结构变化
        checkForComodification();
        // 更新cursor和lastRet
        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的remove方法删除
            ArrayList.this.remove(lastRet);
            // 更新cursor和lastRet
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值