ArrayDeque 是双端队列的动态数组实现,可以当作栈和队列来使用。作为栈时,它的效率比 Stack 更高,作为队列时,效率比 LinkedList 更高。ArrayDeque 大部分操作的时间复杂度都是 O(1)。本文将介绍 ArrayDeque 的核心 API 以及其内部实现的算法。

数据结构

ArrayDeque 的数据结构非常简单,包含一个用于存储数据的 Object 数组和两个分别指向队列头和尾的索引。

transient Object[] elements; // non-private to simplify nested class access
transient int head;
transient int tail;

构造方法

ArrayDeque 有 3 个构造方法。

ArrayDeque()

ArrayDeque() 方法很简单,仅仅是创建了一个长度为 16 的数组 elements。即 ArrayDeque 的容量为 16。

    public ArrayDeque() {
elements = new Object[16];
}

ArrayDeque(int numElements)

此方法传入一个整数 numElements,elements 数组的长度取决于参数 numElements,但 elemenets 的长度不是直接取 numElements,而是通过 calculateSize(int numElements) 计算之后的值。

public ArrayDeque(int numElements) {
allocateElements(numElements);
} private void allocateElements(int numElements) {
elements = new Object[calculateSize(numElements)];
}

calculateSize(int) 是一个静态工具方法,传入一个整数 numElements,返回一个恰好大于 numElements 且为 2 的整数次方的值。注意是大于,例如:输入 4,输出的为 8。这里的算法很巧妙,先通过 5 次位操作将 numElements 最高位左侧的二进制位全部置为 1,再加 1 得到返回值。

事实上 elements 数组在 ArrayDeque 的整个生命周期中长度都为 2 的整数次方。

    private static int calculateSize(int numElements) {
int initialCapacity = MIN_INITIAL_CAPACITY;
// Find the best power of two to hold elements.
// Tests "<=" because arrays aren't kept full.
if (numElements >= initialCapacity) {
initialCapacity = numElements; // 这里的位操作关注 numElements 的最高位即可
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16); // 最高位左侧的二进制位已经全部为 1
initialCapacity++; // 得到一个大于 numElements 且恰好为 2 的整数次幂的数 if (initialCapacity < 0) // 检查是否溢出
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
}
return initialCapacity;
}

ArrayDeque(Collection<? extends E> c)

此方法可以传入一个集合,集合中的元素会按照集合中元素的迭代顺序添加到 ArrayDeque 的尾部。调用的为 addLast(e) 方法。

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

核心 API

ArrayDeque 的 API 是围绕如何在一个动态循环数组进行的一系列操作展开。动态体现在 ArrayDeque 的容量是可以动态扩展的,循环体现在 elements 数组在逻辑上首尾相连接的(物理上 elements 是一个数组,头是 0 ,尾是 elements.length-1)。一系列操作指的是双端队列的操作。

下面通过绘制 ArrayDeque 内部存储结构的方式描述其核心 API 和算法。

实例化

ArrayDeque<Character> deque= new ArrayDeque();

实例化时 head 和 tail 指针均指向了索引 0 位置。head 总是从右往左循环移动,tail 总是从左往右循环移动。头部添加的下一个元素总是存放在 head 下一个移向的索引位置,尾部添加的下一个元素总是存放在当前 tail 所在的位置。

尾部添加 addLast(e)

deque.addLast('A');

addLast(e) 方法往双端队列的尾部添加元素,将元素放入 elements[tail],tail 往右移动一个位置。如果已经 tail 超出了索引范围,则指向 0 。循环利用数组。需要注意的是 ArrayDeque 不能存放 null 元素,不过 Deque 的另一实现 LinkedList 却可以。

JDK 源码使用了高效的位操作实现了这个逻辑。

    public void addLast(E e) {
if (e == null) // ArrayDeque 不能存放元素 null
throw new NullPointerException();
elements[tail] = e;
// 因为 elements.length 总是 2 的整数次方,所以 elements.length - 1 所有二进制位均为 1,
// 若 tail+1 < elements.length,则 (tail + 1) & (elements.length - 1) 结果即为 tail+1;
// 若 tail+1 == elements.length,表示下标越界,则 (tail + 1) & (elements.length - 1) 值为 0
// 这里可以解释为什么长度要设置为 2 的整数次方,目的是为了能够高效地进行位操作
// 后面还判断了是否等于 head,相等则表示 elements 数组放满了,此时触发扩容
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity(); // this.elements 数组长度扩容为原来的两倍
}

头部添加 addFirst(e)

deque.addFirst('B');

与尾部添加类似,不过移动的是 head 指针,head 指针从往左循环移动 1 个位置,放入元素 elements[head](移动指针和放入元素的顺序与 addLast() 相反)。与 addLast(e) 类似,源码中同样使用了高效的位操作来进行移动,进行了扩容判断。

获取头部元素 getFirst() / peekFirst() 和获取尾部元素 getLast() / peekLast()

deque.getFirst(); // 返回 B
deque.getLast(); // 返回 B
deque.peekFirst(); // 返回 A
deque.peekLast(); // 返回 A

从前面尾部添加元素可知,双端队列中有元素的前提下,head 总是指向刚刚往双端队列头部添加的元素的位置,tail 总是指向存放下一个往双端队列添加元素的位置。因此,getFirst() / peekFirst() 返回的是 head 位置的元素,而 getLast() / peekLast() 返回的是上一次 tail 所在元素的位置。

其中 getXXX() 会对获取的值进行判断,若为空,则抛出 NoSuchElementException。前面提到,ArrayDeque 不能存放 null 元素,因此值为空的另一层意思是这个 ArrayDeque 没有存放元素。而 peekXXX() 不会对返回的值进行判断,若获取到 null 则表示这个双端队列中没有元素。可以根据需求来选择相应的 API。

    public E getFirst() {
@SuppressWarnings("unchecked")
E result = (E) elements[head];
if (result == null)
throw new NoSuchElementException();
return result;
} /**
* @throws NoSuchElementException {@inheritDoc}
*/
public E getLast() {
@SuppressWarnings("unchecked")
E result = (E) elements[(tail - 1) & (elements.length - 1)];
if (result == null)
throw new NoSuchElementException();
return result;
} public E peekFirst() {
// elements[head] is null if deque empty
return (E) elements[head];
} @SuppressWarnings("unchecked")
public E peekLast() {
return (E) elements[(tail - 1) & (elements.length - 1)];
}

双端队列中还有若干 API 与上面提到的 API 等价,例如:peek() 与 peekFirst()。更多说明可以参考这篇:Java 双端队列接口 Deque

移除元素 removeFirst() / pollFirst() 和 removeLast() / pollLast()

上面两组 API 中,removeFirst() 调用了 pollFirst(),removeLast() 调用了 pollLast(),只是增加了返回值为空时抛出异常的内容。

与添加元素相反,移除元素时,pollFirst() 先获取当前元素,然后移动 head 指针往右循环移动 1 个位置;pollLast() 则是先向左循环移动 1 个位置,再获取元素。这里需要注意,head 指针和 tail 指针在 ArrayDeque 有元素的时候才移动,没有元素的时候不会移动。

    public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
E result = (E) elements[h];
// Element is null if deque empty
if (result == null) // 返回值 null,不移动 head
return null;
elements[h] = null; // 这一句看起来是多余的,但它的目的是让数组 elements 不再引用移除的对象,这样 GC 就能够将对象回收了。
head = (h + 1) & (elements.length - 1); // head 循环右移 1 个位置
return result;
}
deque.pollLast(); // 返回 'A',tail 往左循环移动 1 个位置,head 不动

deque.pollLast(); // 返回 'B',tail 往左循环移动 1 个位置,head 不动

deque.pollFirst(); // 返回 null, tail 和 head 均不动

扩容

不断调用 addFirst(e) 和 addLast(e) 往里面增加元素,当元素增加到 elements 数组存放不下(即双端队列有元素的情况下 head == tail) 时,ArrayDeque 的容量会自动增加 1 倍。

deque.addLast('A');
deque.addLast('B');
... deque.addLast('M');

elements 数组达到如下状态,此时还差 1 个元素就满了。

再往里添加一个元素。

deque.addFirst('L');

调用 deque.add('L') 时,元素 'L' 先放入到数组中,tail 往右循环移动 1 个位置。然后进行判断,发现 tail 等于 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); // p=head 以及其右边部分先放到新数组左侧
System.arraycopy(elements, 0, a, r, p); // p=head 左边部分尾随。
elements = a;
head = 0;
tail = n;
}

小结

ArrayDeque 在不考虑扩容的情况下,ArrayDeque 头部和尾部操作都仅仅是移动一下索引,效率极高,时间复杂度为 O(1)。

ArrayDeque 的实现中没有线程同步操作,因此 ArrayDeque 是非线程安全的,并发访问一个 ArrayDeque 可能导致错误。

ArrayDeque 存储结构是一个 Object 数组,数组的长度总是 2 的整数次方,这是 ArrayDeque 某些代码能够进行位操作的基础。

ArrayDeque API 与算法分析的更多相关文章

  1. Java 双端队列接口 Deque

    Deque 是一种支持在两端进行操作的线性结构,包含了栈和队列的功能.Java 中建议使用 Dqueue 的实现来替代遗留的 Stack 类.本文将介绍 Deque 提供的主要 API. 双端操作 A ...

  2. ArrayDeque详解

    美人如斯! ArrayDeque是java中对双端队列的线性实现 一.特性 无容量大小限制,容量按需增长: 非线程安全队列,无同步策略,不支持多线程安全访问: 当用作栈时,性能优于Stack,当用于队 ...

  3. 【爬虫集合】抖音API分析

    1. 分析接口 Charles注册码 Registered Name: https://zhile.io License Key: 48891cf209c6d32bf4 抖音API分析 抖音.猫眼网页 ...

  4. 学习JDK1.8集合源码之--ArrayDeque

    1. ArrayDeque简介 ArrayDeque是基于数组实现的一种双端队列,既可以当成普通的队列用(先进先出),也可以当成栈来用(后进先出),故ArrayDeque完全可以代替Stack,Arr ...

  5. LinkedList 的 API 与数据结构

    LinkedList 是 List 接口和 Deque 接口的双向链表实现,它所有的 API 调用都是基于对双向链表的操作.本文将介绍 LinkedList 的数据结构和分析 API 中的算法. 数据 ...

  6. 详解API Gateway流控实现,揭开ROMA平台高性能秒级流控的技术细节

    摘要:ROMA平台的核心系统ROMA Connect源自华为流程IT的集成平台,在华为内部有超过15年的企业业务集成经验. 本文分享自华为云社区<ROMA集成关键技术(1)-API流控技术详解& ...

  7. 硬核 - Java 随机数相关 API 的演进与思考(下)

    本系列将 Java 17 之前的随机数 API 以及 Java 17 之后的统一 API 都做了比较详细的说明,并且将随机数的特性以及实现思路也做了一些简单的分析,帮助大家明白为何会有这么多的随机数算 ...

  8. 详解ROMA Connect API 流控实现技术

    摘要:本文将详细描述API Gateway流控实现,揭开高性能秒级流控的技术细节. 1.概述 ROMA平台的核心系统ROMA Connect源自华为流程IT的集成平台,在华为内部有超过15年的企业业务 ...

  9. 干货来袭-整套完整安全的API接口解决方案

    在各种手机APP泛滥的现在,背后都有同样泛滥的API接口在支撑,其中鱼龙混杂,直接裸奔的WEB API大量存在,安全性令人堪优 在以前WEB API概念没有很普及的时候,都采用自已定义的接口和结构,对 ...

随机推荐

  1. NOI2008 志愿者招聘

    文化课 + 竞赛双废物又来水题解了. 首先,对于题干中的人,很像网络流中的流量,但是他有一个每天人数的下限,我从网上借鉴(chaoxi)到了两种思路: 把下界限制转化为一条边的流量下界,这样就是最小费 ...

  2. Dwango Programming Contest 6th E 题解

    题目大意 你有一条区间\([0, X)\),并且有一个数组\(L_1, ..., L_n\).对于任意\(1 \leq i \leq n\),你可以指定一个非负整数\(0 \leq j_i \leq ...

  3. Java并发编程的艺术(五)——线程和线程的状态

    线程 什么是线程 操作系统调度的最小单元就是线程,也叫轻量级进程. 为什么要使用多线程 多线程程序能够更有效率地利用多处理器核心. 用户响应时间更快. 方便程序员将程序模型映射到Java提供的多线程编 ...

  4. Angular:管道和自定义管道

    ①管道的使用,更多管道在angular官网上有 <p>全部转为大写:{{'hahahah' | uppercase}}</p> <p>保留两位小数:{{1.4555 ...

  5. sqli-labs less1-4(union注入)

    less-1 考点:Single quotes 输入: 判断类型 ?id=1 返回loginname和password.输入的id就是与后台数据库连接的接口通过id=? 查询数据库信息 ?id=1' ...

  6. Kubernetes【K8S】(二):搭建Kubernetes环境

    系统初始化 设置系统时区 # 设置系统时区为 亚洲/上海 [root@k8s-master01 ~]# timedatectl set-timezone Asia/Shanghai # 设置当前得UT ...

  7. 算法(Java实现)—— 分治算法

    分治算法 分治算法的设计模式 基本思想 把复杂问题分解成若干互相独立容易求解的子问题 经典问题 二分搜索 大整数乘法 棋盘覆盖 合并排序 快速排序 线性时间选择 最接近点对问题 循环赛日程表 汉诺塔 ...

  8. 大白话详解大数据hive知识点,老刘真的很用心(3)

    前言:老刘不敢说写的有多好,但敢保证尽量用大白话把自己复习的内容详细解释出来,拒绝资料上的生搬硬套,做到有自己的了解! 1. hive知识点(3) 从这篇文章开始决定进行一些改变,老刘在博客上主要分享 ...

  9. 推荐一款最强Python自动化神器!再也不用写代码了!

    本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理 搞过自动化测试的小伙伴,相信都知道,在Web自动化测试中,有一款自动化测试神器工具: seleniu ...

  10. python极简代码之检测列表是否有重复元素

    极简python代码收集,实战小项目,不断撸码,以防遗忘.持续更新: 1,检测列表是否有重复元素: 1 # !usr/bin/env python3 2 # *-* coding=utf-8 *-* ...