Java——ArrayDeque

ArrayDeque

ArrayDeque有如下构造方法:

java 复制代码
public ArrayDeque()
public ArrayDeque(int numElements)
public ArrayDeque(Collection<? extends E> c)

numElements表示元素个数,初始分配的空间会至少容纳这么多元素,但空间不是正好numElements这么大。

1、实现原理

ArrayDeque内部主要有如下实例变量:

java 复制代码
private transient E[] elements;
private transient int head;
private transient int tail;

elements就是存储元素的数组。ArrayDeque的高效来源于head和tail这两个变量,它们使得物理上简单的从头到尾的数组变为了一个逻辑上循环的数组,避免了在头尾操作时的移动。

1.1、循环数组

对于一般数组,比如arr,第一个元素为arr[0]​,最后一个为arr[arr.length-1]​。但对于ArrayDeque中的数组,它是一个逻辑上的循环数组,所谓循环是指元素到数组尾之后可以接着从数组头开始,数组的长度、第一个和最后一个元素都与head和tail这两个变量有关,具体来说:

  1. 如果head和tail相同,则数组为空,长度为0。
  2. 如果tail大于head,则第一个元素为elements[head],最后一个为elements[tail-1],长度为tail-head,元素索引从head到tail-1。
  3. 如果tail小于head,且为0,则第一个元素为elements[head],最后一个为elements[elements.length-1],元素索引从head到elements.length-1。
  4. 如果tail小于head,且大于0,则会形成循环,第一个元素为elements[head],最后一个是elements[tail-1],元素索引从head到elements.length-1,然后再从0到tail-1。

我们来看一些图示。第一种情况,数组为空,head和tail相同,如图所示。

第二种情况,tail大于head,如图所示,都包含三个元素。

第四种情况,tail不为0,且小于head,如图所示。

1.2、构造方法

默认构造方法的代码为:

java 复制代码
public ArrayDeque() {
    elements = (E[]) new Object[16];
}

分配了一个长度为16的数组。如果有参数numElements,代码为:

java 复制代码
public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

不是简单地分配给定的长度,而是调用了allocateElements。这个方法的代码看上去比较复杂,我们就不列举了,它主要就是在计算应该分配的数组的长度,计算逻辑如下:

  1. 如果numElements小于8,就是8。
  2. 在numElements大于等于8的情况下,分配的实际长度是严格大于numElements并且为2的整数次幂的最小数。比如,如果numElements为10,则实际分配16,如果num-Elements为32,则为64。

为什么要为2的幂次数呢?我们待会会看到,这样会使得很多操作的效率很高。为什么要严格大于numElements呢?因为循环数组必须时刻至少留一个空位,tail变量指向下一个空位,为了容纳numElements个元素,至少需要numElements+1个位置。

看最后一个构造方法:

java 复制代码
public ArrayDeque(Collection<? extends E> c) {
    allocateElements(c.size());
    addAll(c);
}

同样调用allocateElements分配数组,随后调用了addAll,而addAll只是循环调用了add方法。下面我们来看add的实现。

1.3、从尾部添加

add方法的代码为:

java 复制代码
public boolean add(E e) {
    addLast(e);
    return true;
}

addLast的代码为:

java 复制代码
public void addLast(E e) {
    if(e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

将元素添加到tail处,然后tail指向下一个位置,如果队列满了,则调用doubleCapa-city扩展数组。tail的下一个位置是(tail+1) & (elements.length-1),如果与head相同,则队列就满了。

进行与操作保证了索引在正确范围,与(elements.length-1)相与就可以得到下一个正确位置,是因为elements.length是2的幂次方,(elements.length-1)的后几位全是1,无论是正数还是负数,与(elements.length-1)相与都能得到期望的下一个正确位置。

doubleCapacity将数组扩大为两倍,代码为:

java 复制代码
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);
	System.arraycopy(elements, 0, a, r, p);
	elements = (E[])a;
	head = 0;
	tail = n;
}

分配一个长度翻倍的新数组a,将head右边的元素复制到新数组开头处,再复制左边的元素到新数组中,最后重新设置head和tail, head设为0, tail设为n。

我们来看一个例子,假设原长度为8, head和tail为4,现在开始扩大数组,扩大前后的结构如图所示。

add是在末尾添加,我们再看在头部添加的代码。

1.4、从头部添加

addFirst()方法的代码为:

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

在头部添加,要先让head指向前一个位置,然后再赋值给head所在位置。head的前一个位置是(head-1) &(elements.length-1)。刚开始head为0,如果elements.length为8,则(head-1) & (elements.length-1)的结果为7。比如,执行如下代码:

java 复制代码
Deque<String> queue = new ArrayDeque<>(7);
queue.addFirst("a");
queue.addFirst("b");

执行完后,内部结构如图所示。

1.5、从头部删除

removeFirst方法的代码为:

java 复制代码
public E removeFirst() {
    E x = pollFirst();
    if(x == null)
        throw new NoSuchElementException();
    return x;
}

主要调用了pollFirst方法,pollFirst方法的代码为:

java 复制代码
public E pollFirst() {
    int h = head;
    E result = elements[h]; //Element is null if deque empty
    if(result == null)
        return null;
    elements[h] = null;      //Must null out slot
    head = (h + 1) & (elements.length - 1);
    return result;
}

代码比较简单,将原头部位置置为null,然后head置为下一个位置,下一个位置为(h+1) & (elements.length-1)。从尾部删除的代码是类似的,就不赘述了。

1.6、查看长度

ArrayDeque没有单独的字段维护长度,其size方法的代码为:

java 复制代码
public int size() {
    return (tail - head) & (elements.length - 1);
}

通过该方法即可计算出size。

1.7、检查给定元素是否存在

contains方法的代码为:

java 复制代码
public boolean contains(Object o) {
    if(o == null)
        return false;
    int mask = elements.length - 1;
    int i = head;
    E x;
    while( (x = elements[i]) ! = null) {
        if(o.equals(x))
            return true;
        i = (i + 1) & mask;
    }
    return false;
}

就是从head开始遍历并进行对比,循环过程中没有使用tail,而是到元素为null就结束了,这是因为在ArrayDeque中,有效元素不允许为null。

1.8、toArray方法

java 复制代码
public Object[] toArray() {
    return copyElements(new Object[size()]);
}

copyElements的代码为:

java 复制代码
private <T> T[] copyElements(T[] a) {
    if(head < tail) {
        System.arraycopy(elements, head, a, 0, size());
    } else if(head > tail) {
        int headPortionLen = elements.length - head;
        System.arraycopy(elements, head, a, 0, headPortionLen);
        System.arraycopy(elements, 0, a, headPortionLen, tail);
    }
    return a;
}

如果head小于tail,就是从head开始复制size个,否则,复制逻辑与doubleCapacity方法中的类似,先复制从head到末尾的部分,然后复制从0到tail的部分。

2、特点分析

ArrayDeque实现了双端队列,内部使用循环数组实现,这决定了它有如下特点。

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

ArrayDeque和LinkedList都实现了Deque接口,应该用哪一个呢?如果只需要Deque接口,从两端进行操作,一般而言,ArrayDeque效率更高一些,应该被优先使用;如果同时需要根据索引位置进行操作,或者经常需要在中间进行插入和删除,则应该选LinkedList。

相关推荐
NagatoYukee1 小时前
Spring/SpringMVC/SprongBoot知识复习
java·数据库·spring
泓博1 小时前
docker ubuntu源码安装openclaw的常见问题
java·linux·开发语言·ai
YuanDaima20481 小时前
WSL2 核心中间件部署实战:MySQL、Redis 与 RocketMQ
java·数据库·人工智能·redis·python·mysql·rocketmq
南境十里·墨染春水1 小时前
线程池学习(一) 理解 进程 线程 协程及上下文切换
java·开发语言·学习
知兀1 小时前
@Accessors(chain = true)和@Builder链式风格差异
java·开发语言
i220818 Faiz Ul1 小时前
个人健康系统|健康管理|基于java+Android+微信小程序的个人健康系统设计与实现(源码+数据库+文档)
android·java·vue.js·spring boot·微信小程序·毕设·个人健康系统
组合缺一1 小时前
agentscope-harness vs solon-ai-harness:Java 智能体「马具引擎」的双雄对决
java·人工智能·ai·llm·agent·solon·agentscope
Javatutouhouduan10 小时前
2026Java面试的正确打开方式!
java·高并发·java面试·java面试题·后端开发·java编程·java八股文
JAVA面经实录91710 小时前
Java初级最终完整版学习路线图
java·spring·eclipse·maven