ArrayDeque
接口的可调整大小的数组实现Deque。数组双端队列没有容量限制;它们会根据需要增长以支持使用。它们不是线程安全的;在没有外部同步的情况下,它们不支持多线程并发访问。禁止使用空元素。此类可能比 Stack用作堆栈时更快,并且比LinkedList 用作队列时更快。
这个类的iterator方法返回的迭代器是 快速失败的:如果在迭代器创建后的任何时候修改了双端队列,除了通过迭代器自己的remove 方法之外,迭代器通常会抛出一个ConcurrentModificationException. 因此,面对并发修改,迭代器快速而干净地失败,而不是在未来不确定的时间冒任意的、非确定性的行为。
属性
//存储元素的数组
transient Object[] elements;
//队列头位置
transient int head;
//队列尾位置
transient int tail;
//最小初始容量
private static final int MIN_INITIAL_CAPACITY = 8;
使用数组存储元素,并使用头尾指针标识队列的头和尾,其最小容量是8
构造函数
计算容量,这段代码的逻辑是算出大于numElements的最接近的2的n次方且不小于8
比如,3算出来是8,9算出来是16,33算出来是64
默认初始容量是16
,最小容量是8
。
private void allocateElements(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;
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];
}
方法
入队
addFirst()/ offerFirst()方法将一个元素添加到Deque的正面。 同样, *addLast()/ offerLast()*方法将元素添加到末尾。
队头入队
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
//将head指针减1并与数组长度减1取模
//目的是防止数组到头了边界溢出
//如果到头了就从尾部再向前
//相当于循环利用数组
//栗子:head为0,数组长度为8.(-1&7)=(-1%8)=7
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();//如果头尾在一起了,就触发扩容
}
队尾入队
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;// //可以看到tail指针指向的是队列的最后一个元素的下一个位置
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
整个流程:
- 入队有两种方式,从队列头或者从队列尾
- 如果容量不够了,直接扩大为两倍
- 通过取模的方式让头尾指针在数组范围内循环
x & (len - 1) = x % len
,使用&的方式更快
扩容
private void doubleCapacity() {//两倍扩容
assert head == tail;
int p = head;//头指针的位置
int n = elements.length;//旧数组的长度
int r = n - p; // head右边元素的个数
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);//将旧数组head之后的元素拷贝到新数组中
System.arraycopy(elements, 0, a, r, p);//将就数组小标0到head之间的元素拷贝到新数组中
elements = a;//赋值给新数组
head = 0; //head指向0,tail指向旧数组长度表示的位置
tail = n;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sp04qi1H-1652950375638)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220426170635969.png)]
出队
通过remove/pool First/Last进行删除首位元素
队头出队
public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
E result = (E) elements[h];//取队列头元素
// 如果队列为空,就返回null
if (result == null)
return null;
elements[h] = null; //将队列头置为空
head = (h + 1) & (elements.length - 1);//队列头指针想右移动一位
return result;
}
队尾出队
public E pollLast() {
int t = (tail - 1) & (elements.length - 1);//尾指针左移一位
@SuppressWarnings("unchecked")
E result = (E) elements[t];//取当前尾指针处元素
if (result == null)
return null;
elements[t] = null;//将当前尾指针处置为空
tail = t;//tail指向新的尾指针处
return result;
}
总结
- 出队有两种方式,从队列头或者从队列尾
- 通过取模的方式让头尾指针在数组范围内循环
- 出队之后没有缩容
检索
通过peek/peekFirst,peekLast获取头尾值
public E peekFirst() {
// 如果队列是空返回null
return (E) elements[head];
}
作为栈
Deque
可以直接作为栈来使用
import java.util.*;
public class Test {
public static void main(String[] args) {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);//进栈
stack.push(4);
System.out.println(stack.toString());
System.out.println(stack.peek());//获取栈顶元素
stack.pop();//出栈
System.out.println(stack.toString());
}
}
总结
- ArrayDeque是采用数组方式实现的双端队列
- ArrayDeque的出队入队是通过头尾指针循环利用数组实现的
- ArrayDeque容量不足时是会扩容的,每次扩容容量增加一倍
- ArrayDeque可以直接作为栈使用