揭秘 Java ArrayDeque:从源码到原理的深度剖析
一、引言
在 Java 编程的世界里,数据结构是构建高效程序的基石。ArrayDeque
作为 Java 集合框架中的一员,是一个功能强大且性能卓越的双端队列实现。它既可以当作栈来使用,也能作为普通队列使用,还支持从队列的两端进行高效的插入和删除操作。
对于开发者而言,深入理解 ArrayDeque
的使用原理不仅有助于在实际项目中更加合理地运用它,还能提升对 Java 集合框架设计思想的认识。本文将带领大家深入到 ArrayDeque
的源码层面,详细剖析其内部结构、核心操作以及性能特点,让你对 ArrayDeque
有一个全方位的认识。
二、ArrayDeque 概述
2.1 基本概念
ArrayDeque
是一个基于数组实现的双端队列(Deque),它支持在队列的两端进行元素的插入和删除操作。双端队列是一种特殊的队列,它允许在队列的头部和尾部进行高效的元素添加和移除操作,这使得 ArrayDeque
既可以作为栈使用(后进先出,LIFO),也可以作为普通队列使用(先进先出,FIFO)。
2.2 继承关系与接口实现
下面是 ArrayDeque
类的定义以及它的继承关系和接口实现:
java
// ArrayDeque 类继承自 AbstractCollection 类,AbstractCollection 是一个抽象类,实现了 Collection 接口的部分方法
// 同时,ArrayDeque 实现了 Deque 接口,表明它是一个双端队列
// 还实现了 Cloneable 接口,意味着它可以被克隆
// 以及 Serializable 接口,说明它可以被序列化
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable
{
// 类的具体实现将在后续详细分析
}
从上述代码可以看出,ArrayDeque
继承自 AbstractCollection
类,继承了该类中实现的 Collection
接口的部分方法。同时,它实现了 Deque
接口,这使得它具备了双端队列的特性,支持在队列的两端进行元素的操作。此外,它还实现了 Cloneable
和 Serializable
接口,分别支持对象的克隆和序列化。
2.3 与其他队列的对比
在 Java 中,还有其他一些队列实现,如 LinkedList
、PriorityQueue
等,它们与 ArrayDeque
的主要区别如下:
LinkedList
:是一个基于链表实现的双端队列,插入和删除操作的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1),但随机访问的性能较差。而ArrayDeque
基于数组实现,随机访问的性能较好,插入和删除操作在大多数情况下也能达到 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 的时间复杂度。PriorityQueue
:是一个优先队列,元素按照优先级进行排序,每次取出的元素是优先级最高(或最低)的元素。而ArrayDeque
不考虑元素的优先级,只是按照元素的插入顺序进行操作。
三、ArrayDeque 的内部结构
3.1 核心属性
ArrayDeque
类有几个核心属性,用于存储元素和管理队列的状态。以下是这些核心属性的源码及注释:
java
// 存储队列元素的数组,数组的长度总是 2 的幂次方
transient Object[] elements;
// 队列头部元素的索引
transient int head;
// 队列尾部元素的下一个位置的索引
transient int tail;
// 数组的最小初始容量,必须是 2 的幂次方
private static final int MIN_INITIAL_CAPACITY = 8;
elements
:用于存储队列元素的数组,数组的长度总是 2 的幂次方。这是为了方便使用位运算来进行索引的计算,提高性能。head
:队列头部元素的索引,指向队列的第一个元素。tail
:队列尾部元素的下一个位置的索引,指向队列中最后一个元素的下一个位置。MIN_INITIAL_CAPACITY
:数组的最小初始容量,必须是 2 的幂次方,这里设置为 8。
3.2 构造函数
ArrayDeque
类提供了多个构造函数,用于创建不同初始状态的双端队列。以下是几个主要构造函数的源码及注释:
java
// 默认构造函数,创建一个初始容量为 16 的双端队列
public ArrayDeque() {
// 初始化元素数组,初始容量为 16
elements = new Object[16];
}
// 创建一个指定初始容量的双端队列
public ArrayDeque(int numElements) {
// 调用 allocateElements 方法根据指定的元素数量分配合适的数组容量
elements = allocateElements(numElements);
}
// 创建一个包含指定集合元素的双端队列
public ArrayDeque(Collection<? extends E> c) {
// 调用 allocateElements 方法根据集合的大小分配合适的数组容量
elements = allocateElements(c.size());
// 调用 addAll 方法将集合中的元素添加到双端队列中
addAll(c);
}
// 根据指定的元素数量分配合适的数组容量
private static int calculateSize(int numElements) {
// 最小初始容量为 8
int initialCapacity = MIN_INITIAL_CAPACITY;
// 如果指定的元素数量大于等于最小初始容量
if (numElements >= initialCapacity) {
// 初始化初始容量为指定元素数量
initialCapacity = numElements;
// 将初始容量调整为 2 的幂次方
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++;
// 如果调整后的容量小于 0,将容量缩小一半
if (initialCapacity < 0)
initialCapacity >>>= 1;
}
return initialCapacity;
}
// 分配存储元素的数组
private Object[] allocateElements(int numElements) {
// 调用 calculateSize 方法计算合适的数组容量
int initialCapacity = calculateSize(numElements);
// 创建指定容量的数组
return new Object[initialCapacity];
}
这些构造函数提供了多种方式来创建 ArrayDeque
。默认构造函数创建一个初始容量为 16 的双端队列;ArrayDeque(int numElements)
构造函数根据指定的元素数量分配合适的数组容量;ArrayDeque(Collection<? extends E> c)
构造函数创建一个包含指定集合元素的双端队列。calculateSize
方法用于将指定的元素数量调整为 2 的幂次方,以满足数组容量的要求。
3.3 循环数组结构
ArrayDeque
使用循环数组来实现双端队列的功能。循环数组是一种特殊的数组结构,当数组的尾部达到数组的末尾时,下一个元素会插入到数组的头部,形成一个循环。在 ArrayDeque
中,通过 head
和 tail
两个索引来管理循环数组的头部和尾部。
例如,当 tail
索引达到数组的末尾时,下一个元素会插入到数组的头部,即 tail = 0
。同样,当 head
索引达到数组的末尾时,下一个元素会从数组的头部开始移除。通过这种方式,ArrayDeque
可以高效地在队列的两端进行元素的插入和删除操作。
以下是一个简单的示例,展示了循环数组的工作原理:
java
// 假设我们有一个长度为 8 的数组
Object[] array = new Object[8];
// 初始化 head 和 tail 索引
int head = 0;
int tail = 0;
// 向队列中添加元素
array[tail] = "Element 1";
tail = (tail + 1) & (array.length - 1); // 使用位运算计算新的 tail 索引
array[tail] = "Element 2";
tail = (tail + 1) & (array.length - 1);
// 从队列中移除元素
Object element = array[head];
head = (head + 1) & (array.length - 1); // 使用位运算计算新的 head 索引
System.out.println("Removed element: " + element);
在上述示例中,使用位运算 (index + 1) & (array.length - 1)
来计算新的 head
和 tail
索引,确保索引在数组的有效范围内循环。
四、基本操作的源码分析
4.1 插入操作
4.1.1 addFirst(E e) 方法
addFirst(E e)
方法用于在队列的头部插入一个元素。以下是该方法的源码及注释:
java
// 在队列的头部插入一个元素
public void addFirst(E e) {
// 如果插入的元素为 null,抛出 NullPointerException 异常
if (e == null)
throw new NullPointerException();
// 计算新的 head 索引,使用位运算确保索引在数组范围内循环
elements[head = (head - 1) & (elements.length - 1)] = e;
// 如果 head 索引等于 tail 索引,说明数组已满,需要进行扩容
if (head == tail)
doubleCapacity();
}
- 首先,检查插入的元素是否为
null
,如果为null
,抛出NullPointerException
异常。 - 然后,使用
(head - 1) & (elements.length - 1)
计算新的head
索引,将元素插入到新的head
位置。 - 最后,检查
head
索引是否等于tail
索引,如果相等,说明数组已满,调用doubleCapacity
方法进行扩容。
4.1.2 addLast(E e) 方法
addLast(E e)
方法用于在队列的尾部插入一个元素。以下是该方法的源码及注释:
java
// 在队列的尾部插入一个元素
public void addLast(E e) {
// 如果插入的元素为 null,抛出 NullPointerException 异常
if (e == null)
throw new NullPointerException();
// 将元素插入到当前 tail 位置
elements[tail] = e;
// 计算新的 tail 索引,使用位运算确保索引在数组范围内循环
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
// 如果新的 tail 索引等于 head 索引,说明数组已满,需要进行扩容
doubleCapacity();
}
- 首先,检查插入的元素是否为
null
,如果为null
,抛出NullPointerException
异常。 - 然后,将元素插入到当前
tail
位置。 - 接着,使用
(tail + 1) & (elements.length - 1)
计算新的tail
索引。 - 最后,检查新的
tail
索引是否等于head
索引,如果相等,说明数组已满,调用doubleCapacity
方法进行扩容。
4.1.3 doubleCapacity() 方法
doubleCapacity()
方法用于对存储元素的数组进行扩容。以下是该方法的源码及注释:
java
// 对存储元素的数组进行扩容
private void doubleCapacity() {
// 断言 head 索引等于 tail 索引,确保数组已满
assert head == tail;
// 获取旧数组的长度
int p = head;
int n = elements.length;
// 计算旧数组中从 head 到数组末尾的元素数量
int r = n - p;
// 新数组的长度为旧数组长度的 2 倍
int newCapacity = n << 1;
// 如果新数组的长度小于 0,说明发生了溢出,抛出 IllegalStateException 异常
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 数组为新数组
elements = a;
// 更新 head 索引为 0
head = 0;
// 更新 tail 索引为旧数组的长度
tail = n;
}
- 首先,使用
assert
语句确保head
索引等于tail
索引,即数组已满。 - 然后,计算旧数组中从
head
到数组末尾的元素数量r
。 - 接着,将新数组的长度设置为旧数组长度的 2 倍。
- 如果新数组的长度小于 0,说明发生了溢出,抛出
IllegalStateException
异常。 - 创建新数组,并使用
System.arraycopy
方法将旧数组中的元素复制到新数组中。 - 最后,更新
elements
数组为新数组,将head
索引设置为 0,将tail
索引设置为旧数组的长度。
4.2 删除操作
4.2.1 removeFirst() 方法
removeFirst()
方法用于移除并返回队列的第一个元素。以下是该方法的源码及注释:
java
// 移除并返回队列的第一个元素
public E removeFirst() {
// 调用 pollFirst 方法移除并返回队列的第一个元素
E x = pollFirst();
// 如果返回的元素为 null,说明队列为空,抛出 NoSuchElementException 异常
if (x == null)
throw new NoSuchElementException();
return x;
}
- 该方法调用
pollFirst
方法移除并返回队列的第一个元素。 - 如果返回的元素为
null
,说明队列为空,抛出NoSuchElementException
异常。
4.2.2 removeLast() 方法
removeLast()
方法用于移除并返回队列的最后一个元素。以下是该方法的源码及注释:
java
// 移除并返回队列的最后一个元素
public E removeLast() {
// 调用 pollLast 方法移除并返回队列的最后一个元素
E x = pollLast();
// 如果返回的元素为 null,说明队列为空,抛出 NoSuchElementException 异常
if (x == null)
throw new NoSuchElementException();
return x;
}
- 该方法调用
pollLast
方法移除并返回队列的最后一个元素。 - 如果返回的元素为
null
,说明队列为空,抛出NoSuchElementException
异常。
4.2.3 pollFirst() 方法
pollFirst()
方法用于移除并返回队列的第一个元素,如果队列为空,则返回 null
。以下是该方法的源码及注释:
java
// 移除并返回队列的第一个元素,如果队列为空,则返回 null
@SuppressWarnings("unchecked")
public E pollFirst() {
// 获取 head 索引
int h = head;
// 获取当前 head 位置的元素
E result = (E) elements[h];
// 如果当前 head 位置的元素为 null,说明队列为空,返回 null
if (result == null)
return null;
// 将当前 head 位置的元素置为 null,帮助垃圾回收
elements[h] = null;
// 计算新的 head 索引,使用位运算确保索引在数组范围内循环
head = (h + 1) & (elements.length - 1);
return result;
}
- 首先,获取
head
索引和当前head
位置的元素。 - 如果当前
head
位置的元素为null
,说明队列为空,返回null
。 - 然后,将当前
head
位置的元素置为null
,帮助垃圾回收。 - 接着,使用
(h + 1) & (elements.length - 1)
计算新的head
索引。 - 最后,返回移除的元素。
4.2.4 pollLast() 方法
pollLast()
方法用于移除并返回队列的最后一个元素,如果队列为空,则返回 null
。以下是该方法的源码及注释:
java
// 移除并返回队列的最后一个元素,如果队列为空,则返回 null
@SuppressWarnings("unchecked")
public E pollLast() {
// 计算旧的 tail 索引
int t = (tail - 1) & (elements.length - 1);
// 获取旧的 tail 位置的元素
E result = (E) elements[t];
// 如果旧的 tail 位置的元素为 null,说明队列为空,返回 null
if (result == null)
return null;
// 将旧的 tail 位置的元素置为 null,帮助垃圾回收
elements[t] = null;
// 更新 tail 索引为旧的 tail 索引
tail = t;
return result;
}
- 首先,使用
(tail - 1) & (elements.length - 1)
计算旧的tail
索引。 - 然后,获取旧的
tail
位置的元素。 - 如果旧的
tail
位置的元素为null
,说明队列为空,返回null
。 - 接着,将旧的
tail
位置的元素置为null
,帮助垃圾回收。 - 最后,更新
tail
索引为旧的tail
索引,并返回移除的元素。
4.3 查看操作
4.3.1 getFirst() 方法
getFirst()
方法用于查看队列的第一个元素,但不移除该元素。以下是该方法的源码及注释:
java
// 查看队列的第一个元素,但不移除该元素
@SuppressWarnings("unchecked")
public E getFirst() {
// 获取 head 位置的元素
E x = (E) elements[head];
// 如果 head 位置的元素为 null,说明队列为空,抛出 NoSuchElementException 异常
if (x == null)
throw new NoSuchElementException();
return x;
}
- 该方法获取
head
位置的元素。 - 如果
head
位置的元素为null
,说明队列为空,抛出NoSuchElementException
异常。
4.3.2 getLast() 方法
getLast()
方法用于查看队列的最后一个元素,但不移除该元素。以下是该方法的源码及注释:
java
// 查看队列的最后一个元素,但不移除该元素
@SuppressWarnings("unchecked")
public E getLast() {
// 计算旧的 tail 索引
E x = (E) elements[(tail - 1) & (elements.length - 1)];
// 如果旧的 tail 位置的元素为 null,说明队列为空,抛出 NoSuchElementException 异常
if (x == null)
throw new NoSuchElementException();
return x;
}
- 该方法使用
(tail - 1) & (elements.length - 1)
计算旧的tail
索引,并获取该位置的元素。 - 如果旧的
tail
位置的元素为null
,说明队列为空,抛出NoSuchElementException
异常。
4.3.3 peekFirst() 方法
peekFirst()
方法用于查看队列的第一个元素,但不移除该元素,如果队列为空,则返回 null
。以下是该方法的源码及注释:
java
// 查看队列的第一个元素,但不移除该元素,如果队列为空,则返回 null
@SuppressWarnings("unchecked")
public E peekFirst() {
// 返回 head 位置的元素,如果为 null,说明队列为空
return (E) elements[head];
}
- 该方法直接返回
head
位置的元素,如果该元素为null
,说明队列为空。
4.3.4 peekLast() 方法
peekLast()
方法用于查看队列的最后一个元素,但不移除该元素,如果队列为空,则返回 null
。以下是该方法的源码及注释:
java
// 查看队列的最后一个元素,但不移除该元素,如果队列为空,则返回 null
@SuppressWarnings("unchecked")
public E peekLast() {
// 计算旧的 tail 索引,并返回该位置的元素,如果为 null,说明队列为空
return (E) elements[(tail - 1) & (elements.length - 1)];
}
- 该方法使用
(tail - 1) & (elements.length - 1)
计算旧的tail
索引,并返回该位置的元素,如果该元素为null
,说明队列为空。
4.4 判断队列是否为空(isEmpty)
4.4.1 isEmpty() 方法
isEmpty()
方法用于判断队列是否为空。以下是该方法的源码及注释:
java
// 判断队列是否为空
public boolean isEmpty() {
// 如果 head 索引等于 tail 索引,说明队列为空,返回 true,否则返回 false
return head == tail;
}
- 该方法通过比较
head
索引和tail
索引是否相等来判断队列是否为空。
4.5 获取队列元素数量(size)
4.5.1 size() 方法
size()
方法用于获取队列中元素的数量。以下是该方法的源码及注释:
java
// 获取队列中元素的数量
public int size() {
// 使用位运算计算队列中元素的数量
return (tail - head) & (elements.length - 1);
}
- 该方法使用
(tail - head) & (elements.length - 1)
计算队列中元素的数量,通过位运算确保结果在数组的有效范围内。
五、迭代器的实现
5.1 迭代器接口
ArrayDeque
类实现了 Iterable
接口,因此可以使用 iterator()
方法获取一个迭代器来遍历队列中的元素。以下是 iterator()
方法的源码及注释:
java
// 获取一个迭代器,用于遍历队列中的元素
public Iterator<E> iterator() {
// 返回一个 DeqIterator 对象
return new DeqIterator();
}
// 内部类 DeqIterator 实现了 Iterator 接口,用于迭代 ArrayDeque 中的元素
private class DeqIterator implements Iterator<E> {
// 下一个要返回的元素的索引
private int cursor = head;
// 最后一次返回的元素的索引
private int fence = tail;
// 最后一次返回的元素的索引,如果为 -1,表示没有返回过元素
private int lastRet = -1;
// 判断是否还有下一个元素
public boolean hasNext() {
// 如果 cursor 不等于 fence,说明还有下一个元素,返回 true,否则返回 false
return cursor != fence;
}
// 获取下一个元素
@SuppressWarnings("unchecked")
public E next() {
// 如果 cursor 等于 fence,说明没有下一个元素,抛出 NoSuchElementException 异常
if (cursor == fence)
throw new NoSuchElementException();
// 获取当前 cursor 位置的元素
E result = (E) elements[cursor];
// 如果当前 cursor 位置的元素不等于 null,更新 lastRet 为 cursor
if ( (lastRet = cursor) != fence)
// 更新 cursor 索引,使用位运算确保索引在数组范围内循环
cursor = (cursor + 1) & (elements.length - 1);
return result;
}
// 删除当前迭代的元素
public void remove() {
// 如果 lastRet 为 -1,说明还没有调用过 next 方法,抛出 IllegalStateException 异常
if (lastRet < 0)
throw new IllegalStateException();
// 移除 lastRet 位置的元素
delete(lastRet);
// 如果 lastRet 等于 cursor,说明移除的是当前 cursor 位置的元素,更新 cursor 为 lastRet
if (lastRet < cursor)
cursor = (cursor - 1) & (elements.length - 1);
// 将 lastRet 置为 -1
lastRet = -1;
}
// 删除指定位置的元素
private void delete(int i) {
// 断言 i 索引在有效范围内
assert head != tail;
// 计算要移动的元素数量
int numMoved;
if (i < head) {
// 如果 i 索引在 head 之前,计算从 i 到 head 的元素数量
numMoved = head - i - 1;
// 将从 i + 1 到 head 的元素向前移动一位
if (numMoved > 0)
System.arraycopy(elements, i + 1, elements, i, numMoved);
// 将 head 位置的元素置为 null
elements[head] = null;
// 更新 head 索引,使用位运算确保索引在数组范围内循环
head = (head - 1) & (elements.length - 1);
} else {
// 如果 i 索引在 head 之后,计算从 i + 1 到 tail 的元素数量
numMoved = tail - i - 1;
// 将从 i + 1 到 tail 的元素向后移动一位
if (numMoved > 0)
System.arraycopy(elements, i, elements, i + 1, numMoved);
// 更新 tail 索引,使用位运算确保索引在数组范围内循环
tail = (tail - 1) & (elements.length - 1);
}
// 如果队列中只有一个元素,将 head 和 tail 索引都置为 0
if (head == tail)
tail = 0;
}
}
iterator()
方法返回一个DeqIterator
对象,用于迭代ArrayDeque
中的元素。DeqIterator
类实现了Iterator
接口,提供了以下方法:hasNext()
:判断是否还有下一个元素。next()
:获取下一个元素。在获取元素之前,会检查是否还有下一个元素,如果没有则抛出NoSuchElementException
异常。remove()
:删除当前迭代的元素。在删除元素之前,会检查是否已经调用过next
方法,如果没有则抛出IllegalStateException
异常。
5.2 迭代顺序
ArrayDeque
的迭代器按照队列中元素的顺序进行迭代,从队列的头部开始,依次向后遍历。例如,以下是一个使用迭代器遍历 ArrayDeque
的示例代码:
java
import java.util.ArrayDeque;
import java.util.Iterator;
public class ArrayDequeIterationExample {
public static void main(String[] args) {
// 创建一个 ArrayDeque 对象
ArrayDeque<Integer> deque = new ArrayDeque<>();
// 向队列中添加元素
deque.addFirst(3);
deque.addFirst(2);
deque.addFirst(1);
// 使用迭代器遍历队列中的元素
Iterator<Integer> iterator = deque.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
在上述示例代码中,首先创建一个 ArrayDeque
对象,并向队列的头部添加元素。然后使用迭代器遍历队列中的元素,迭代器会按照元素的插入顺序依次输出元素。
六、线程安全机制
6.1 非线程安全
ArrayDeque
是非线程安全的,这意味着在多线程环境下,如果多个线程同时对 ArrayDeque
进行插入、删除等操作,可能会导致数据不一致或抛出异常。例如,一个线程正在进行插入操作,而另一个线程同时进行删除操作,可能会破坏循环数组的结构,导致元素的顺序混乱。
6.2 替代方案
如果需要在多线程环境下使用双端队列,可以使用 LinkedBlockingDeque
类。LinkedBlockingDeque
是 Java 并发包中的一个类,它实现了 BlockingDeque
接口,是线程安全的双端队列。以下是一个使用 LinkedBlockingDeque
的示例代码:
java
import java.util.concurrent.LinkedBlockingDeque;
public class LinkedBlockingDequeExample {
public static void main(String[] args) {
// 创建一个 LinkedBlockingDeque 对象
LinkedBlockingDeque<Integer> deque = new LinkedBlockingDeque<>();
// 向队列中添加元素
deque.addFirst(3);
deque.addFirst(2);
deque.addFirst(1);
// 从队列中取出元素
while (!deque.isEmpty()) {
System.out.println(deque.pollFirst());
}
}
}
在上述示例代码中,创建了一个 LinkedBlockingDeque
对象,并向队列的头部添加元素。然后使用 pollFirst
方法从队列的头部取出元素,LinkedBlockingDeque
会保证在多线程环境下操作的线程安全。
七、性能分析
7.1 时间复杂度分析
- 插入操作 :在队列的头部或尾部插入元素的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。这是因为
ArrayDeque
使用循环数组实现,只需要更新head
或tail
索引即可。 - 删除操作 :在队列的头部或尾部删除元素的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。同样,只需要更新
head
或tail
索引,并将相应位置的元素置为null
。 - 查看操作 :查看队列的第一个或最后一个元素的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。只需要直接访问
head
或tail
索引对应的元素。 - 迭代操作 :使用迭代器遍历队列中的元素的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 是队列中元素的数量。因为需要依次访问队列中的每个元素。
7.2 空间复杂度分析
ArrayDeque
的空间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 是队列中元素的数量。这是因为需要使用一个数组来存储队列中的元素,数组的长度至少为 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n。
7.3 与其他数据结构的性能对比
- 与
LinkedList
的对比 :- 插入和删除操作 :
ArrayDeque
和LinkedList
在队列的头部和尾部插入和删除元素的时间复杂度都是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。但ArrayDeque
基于数组实现,随机访问的性能较好,而LinkedList
基于链表实现,随机访问的性能较差。 - 空间复杂度 :
ArrayDeque
的空间复杂度相对较低,因为它使用数组存储元素,而LinkedList
需要额外的指针来维护链表结构,空间开销较大。
- 插入和删除操作 :
- 与
ArrayList
的对比 :- 插入和删除操作 :
ArrayList
在数组的末尾插入和删除元素的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1),但在数组的头部插入和删除元素的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n),因为需要移动大量元素。而ArrayDeque
在队列的头部和尾部插入和删除元素的时间复杂度都是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。 - 随机访问 :
ArrayList
支持随机访问,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1),而ArrayDeque
不支持随机访问。
- 插入和删除操作 :
八、应用场景
8.1 栈的实现
ArrayDeque
可以作为栈来使用,实现后进先出(LIFO)的功能。以下是一个使用 ArrayDeque
实现栈的示例代码:
java
import java.util.ArrayDeque;
public class StackExample {
private ArrayDeque<Integer> stack;
public StackExample() {
// 初始化栈
stack = new ArrayDeque<>();
}
// 入栈操作
public void push(int element) {
// 在队列的头部插入元素
stack.addFirst(element);
}
// 出栈操作
public int pop() {
// 移除并返回队列的第一个元素
return stack.removeFirst();
}
// 查看栈顶元素
public int peek() {
// 查看队列的第一个元素
return stack.getFirst();
}
// 判断栈是否为空
public boolean isEmpty() {
// 判断队列是否为空
return stack.isEmpty();
}
public static void main(String[] args) {
StackExample stack = new StackExample();
// 入栈操作
stack.push(1);
stack.push(2);
stack.push(3);
// 出栈操作
while (!stack.isEmpty()) {
System.out.println(stack.pop());
}
}
}
在这个示例中,使用 ArrayDeque
的 addFirst
方法实现入栈操作,使用 removeFirst
方法实现出栈操作,使用 getFirst
方法查看栈顶元素。通过这种方式,ArrayDeque
可以很好地模拟栈的功能。
8.2 队列的实现
ArrayDeque
也可以作为普通队列来使用,实现先进先出(FIFO)的功能。以下是一个使用 ArrayDeque
实现队列的示例代码:
java
import java.util.ArrayDeque;
public class QueueExample {
private ArrayDeque<Integer> queue;
public QueueExample() {
// 初始化队列
queue = new ArrayDeque<>();
}
// 入队操作
public void enqueue(int element) {
// 在队列的尾部插入元素
queue.addLast(element);
}
// 出队操作
public int dequeue() {
// 移除并返回队列的第一个元素
return queue.removeFirst();
}
// 查看队首元素
public int peek() {
// 查看队列的第一个元素
return queue.getFirst();
}
// 判断队列是否为空
public boolean isEmpty() {
// 判断队列是否为空
return queue.isEmpty();
}
public static void main(String[] args) {
QueueExample queue = new QueueExample();
// 入队操作
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
// 出队操作
while (!queue.isEmpty()) {
System.out.println(queue.dequeue());
}
}
}
在这个示例中,使用 ArrayDeque
的 addLast
方法实现入队操作,使用 removeFirst
方法实现出队操作,使用 `
滑动窗口问题是算法中常见的一类问题,例如在一个数组中,计算固定大小窗口内的最大值、最小值等。ArrayDeque
可以很好地解决这类问题,利用其双端队列的特性,在 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 的时间复杂度内完成计算。
下面以计算数组中每个固定大小窗口内的最大值为例,详细介绍 ArrayDeque
在滑动窗口问题中的应用。
java
import java.util.ArrayDeque;
public class SlidingWindowMaximum {
// 该方法用于计算数组中每个固定大小窗口内的最大值
public static int[] maxSlidingWindow(int[] nums, int k) {
// 如果输入数组为空或者窗口大小小于等于 0,直接返回空数组
if (nums == null || k <= 0) {
return new int[0];
}
// 结果数组的长度为输入数组长度减去窗口大小加 1
int n = nums.length;
int[] result = new int[n - k + 1];
int index = 0;
// 创建一个 ArrayDeque 用于存储数组元素的索引
ArrayDeque<Integer> deque = new ArrayDeque<>();
// 遍历输入数组
for (int i = 0; i < n; i++) {
// 移除已经不在当前窗口内的元素的索引
if (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
deque.pollFirst();
}
// 移除队列中小于当前元素的元素的索引
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
// 将当前元素的索引加入队列尾部
deque.offerLast(i);
// 当窗口大小达到 k 时,记录当前窗口的最大值
if (i >= k - 1) {
result[index++] = nums[deque.peekFirst()];
}
}
return result;
}
public static void main(String[] args) {
int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
int k = 3;
int[] result = maxSlidingWindow(nums, k);
// 输出每个窗口内的最大值
for (int num : result) {
System.out.print(num + " ");
}
}
}
代码解释:
-
初始化:
- 首先检查输入数组是否为空或者窗口大小是否小于等于 0,如果是,则直接返回空数组。
- 创建一个长度为
n - k + 1
的结果数组,用于存储每个窗口内的最大值。 - 创建一个
ArrayDeque
用于存储数组元素的索引。
-
遍历数组:
- 对于每个元素,先检查队列头部的索引是否已经不在当前窗口内,如果是,则将其从队列头部移除。
- 然后从队列尾部开始,移除所有小于当前元素的元素的索引,这样可以保证队列头部的元素始终是当前窗口内的最大值。
- 将当前元素的索引加入队列尾部。
-
记录最大值:
- 当遍历到的元素数量达到窗口大小
k
时,将队列头部的元素(即当前窗口内的最大值)记录到结果数组中。
- 当遍历到的元素数量达到窗口大小
通过这种方式,ArrayDeque
可以高效地解决滑动窗口问题,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n),因为每个元素最多入队和出队一次。
8.4 广度优先搜索(BFS)
广度优先搜索是一种用于遍历或搜索树或图的算法,它从根节点(或起始节点)开始,逐层地访问节点。在 BFS 中,通常使用队列来存储待访问的节点。ArrayDeque
可以作为 BFS 中的队列使用,实现高效的节点访问。
下面是一个使用 ArrayDeque
实现图的 BFS 遍历的示例代码:
java
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
// 图类
class Graph {
private int V; // 顶点数
private List<List<Integer>> adj; // 邻接表
// 构造函数,初始化图
public Graph(int v) {
V = v;
adj = new ArrayList<>(v);
for (int i = 0; i < v; i++) {
adj.add(new ArrayList<>());
}
}
// 添加边
public void addEdge(int u, int v) {
adj.get(u).add(v);
}
// 广度优先搜索
public void BFS(int s) {
// 标记数组,用于标记节点是否已被访问
boolean[] visited = new boolean[V];
// 创建一个 ArrayDeque 作为队列
ArrayDeque<Integer> queue = new ArrayDeque<>();
// 标记起始节点为已访问,并将其加入队列
visited[s] = true;
queue.offerLast(s);
while (!queue.isEmpty()) {
// 从队列头部取出一个节点
int u = queue.pollFirst();
System.out.print(u + " ");
// 遍历该节点的所有邻接节点
for (int v : adj.get(u)) {
if (!visited[v]) {
// 如果邻接节点未被访问,标记为已访问,并将其加入队列
visited[v] = true;
queue.offerLast(v);
}
}
}
}
public static void main(String[] args) {
Graph g = new Graph(4);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 0);
g.addEdge(2, 3);
g.addEdge(3, 3);
System.out.println("Following is Breadth First Traversal " +
"(starting from vertex 2)");
g.BFS(2);
}
}
代码解释:
-
图的初始化:
- 定义图的顶点数
V
和邻接表adj
。 - 构造函数中初始化邻接表。
addEdge
方法用于添加边。
- 定义图的顶点数
-
BFS 实现:
- 创建一个布尔类型的标记数组
visited
,用于标记节点是否已被访问。 - 创建一个
ArrayDeque
作为队列,将起始节点标记为已访问并加入队列。 - 当队列不为空时,从队列头部取出一个节点,访问该节点,并将其所有未访问的邻接节点标记为已访问并加入队列。
- 创建一个布尔类型的标记数组
通过使用 ArrayDeque
作为队列,BFS 算法可以高效地进行节点的访问和遍历。
九、常见问题及解决方案
9.1 数组扩容问题
9.1.1 问题描述
ArrayDeque
在插入元素时,如果数组已满,会调用 doubleCapacity
方法进行扩容,将数组长度扩大为原来的 2 倍。频繁的扩容操作会导致性能下降,因为需要进行数组的复制操作。
9.1.2 解决方案
- 预估初始容量 :在创建
ArrayDeque
时,根据实际需求预估初始容量,避免频繁扩容。例如,如果已知需要存储 100 个元素,可以使用ArrayDeque(int numElements)
构造函数创建一个初始容量为 128(大于 100 的最小 2 的幂次方)的ArrayDeque
。
java
import java.util.ArrayDeque;
public class EstimateCapacityExample {
public static void main(String[] args) {
// 预估需要存储 100 个元素,创建初始容量为 128 的 ArrayDeque
ArrayDeque<Integer> deque = new ArrayDeque<>(128);
for (int i = 0; i < 100; i++) {
deque.addLast(i);
}
}
}
- 批量插入 :如果需要插入大量元素,可以考虑批量插入,减少扩容次数。例如,先将元素存储在一个临时数组或集合中,然后一次性将这些元素添加到
ArrayDeque
中。
9.2 并发修改问题
9.2.1 问题描述
由于 ArrayDeque
是非线程安全的,在多线程环境下,如果多个线程同时对 ArrayDeque
进行插入、删除等操作,可能会导致数据不一致或抛出异常。例如,一个线程正在进行插入操作,而另一个线程同时进行删除操作,可能会破坏循环数组的结构,导致元素的顺序混乱。
9.2.2 解决方案
- 使用线程安全的替代类 :如前面提到的,可以使用
LinkedBlockingDeque
类,它是 Java 并发包中的一个类,实现了BlockingDeque
接口,是线程安全的双端队列。
java
import java.util.concurrent.LinkedBlockingDeque;
public class ThreadSafeDequeExample {
public static void main(String[] args) {
// 创建一个线程安全的双端队列
LinkedBlockingDeque<Integer> deque = new LinkedBlockingDeque<>();
// 向队列中添加元素
deque.addFirst(3);
deque.addFirst(2);
deque.addFirst(1);
// 从队列中取出元素
while (!deque.isEmpty()) {
System.out.println(deque.pollFirst());
}
}
}
- 同步机制 :如果需要继续使用
ArrayDeque
,可以使用同步机制,如synchronized
关键字或ReentrantLock
来保证线程安全。
java
import java.util.ArrayDeque;
import java.util.concurrent.locks.ReentrantLock;
public class SynchronizedArrayDequeExample {
private ArrayDeque<Integer> deque = new ArrayDeque<>();
private ReentrantLock lock = new ReentrantLock();
// 同步的插入方法
public void addElement(int element) {
lock.lock();
try {
deque.addLast(element);
} finally {
lock.unlock();
}
}
// 同步的删除方法
public Integer removeElement() {
lock.lock();
try {
return deque.pollFirst();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
SynchronizedArrayDequeExample example = new SynchronizedArrayDequeExample();
example.addElement(1);
example.addElement(2);
System.out.println(example.removeElement());
}
}
9.3 元素为 null 的问题
9.3.1 问题描述
ArrayDeque
不允许插入 null
元素,当尝试插入 null
元素时,会抛出 NullPointerException
异常。
9.3.2 解决方案
- 检查元素是否为 null :在插入元素之前,检查元素是否为
null
,如果为null
,可以选择忽略该元素或进行其他处理。
java
import java.util.ArrayDeque;
public class NullElementCheckExample {
public static void main(String[] args) {
ArrayDeque<Integer> deque = new ArrayDeque<>();
Integer element = null;
if (element != null) {
deque.addLast(element);
} else {
System.out.println("Element is null, skipping insertion.");
}
}
}
十、总结与展望
10.1 总结
ArrayDeque
是 Java 集合框架中一个功能强大且性能卓越的双端队列实现。它基于循环数组实现,具有以下特点:
- 高效的插入和删除操作 :在队列的头部和尾部插入和删除元素的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1),这使得
ArrayDeque
非常适合实现栈和队列等数据结构。 - 随机访问性能较好 :由于使用数组存储元素,
ArrayDeque
在一定程度上支持随机访问,虽然没有ArrayList
那样直接的随机访问方法,但通过索引计算可以实现相对高效的访问。 - 不允许存储 null 元素 :这一限制可以避免在使用过程中出现
NullPointerException
异常,提高程序的健壮性。 - 非线程安全 :在多线程环境下使用时需要注意线程安全问题,可以使用
LinkedBlockingDeque
等线程安全的替代类或使用同步机制。
10.2 展望
随着 Java 技术的不断发展,ArrayDeque
可能会在以下方面得到进一步的优化和扩展:
- 性能优化 :在未来的 Java 版本中,可能会对
ArrayDeque
的实现进行优化,进一步提高插入、删除和扩容等操作的性能。例如,采用更高效的数组复制算法或减少内存开销。 - 并发支持 :虽然目前有
LinkedBlockingDeque
等线程安全的替代类,但在某些特定的并发场景下,可能还需要更高效的并发ArrayDeque
实现。未来可能会引入新的并发数据结构或对ArrayDeque
进行改进,以支持更高效的并发操作。 - 功能扩展 :可能会为
ArrayDeque
增加更多的功能,如支持批量插入和删除操作、支持元素的更新操作等,以提高其使用的灵活性。 - 与其他数据结构的集成 :
ArrayDeque
可能会与其他数据结构进行更紧密的集成,例如与图算法、排序算法等结合,提供更强大的功能。
总之,ArrayDeque
作为 Java 集合框架中的重要组成部分,在未来的发展中有望为开发者提供更高效、更强大的功能,帮助开发者更好地解决各种实际问题。同时,开发者也应该根据具体的需求,合理选择和使用 ArrayDeque
以及其他相关的数据结构,以提高程序的性能和可维护性。通过对 ArrayDeque
的深入理解和应用,我们可以在 Java 编程中更加得心应手地处理各种数据结构和算法问题。