这篇文章介绍java的数据结构之双端队列之ArrayDeque
1、ArrayDeque
ArrayDeque是一个双端数组,既可以当做栈使用,也可以当做队列使用。
ArrayDeque有两个指针head和tail分别指向数据开头和数据结尾,当你从一端进行数据的插入和弹出时,它可以当做栈使用;当你从一端进行数据的插入,从另一端进行数据的弹出时,它可以当做队列使用;当然也可以从两端同时进行数据的插入和弹出,这种情况就比较灵活了,具体在使用在什么场景下,目前还不太清楚,带我仔细研究后再加说明。
本篇文章从源代码的角度讨论ArrayDeque的数据结构,以及数据的插入和弹出操作。
2、ArrayDeque的初始化
(1)、当你不指定ArrayDeque数组的长度时,默认长度是16
(2)、当你指定ArrayDeque数组的长度时,代码会将数组长度转化为2的整次幂。比如如果你传的长度是18,则会转化为比18大但是2的整次幂的数32,也就是实际数组的长度为32。
注:数组长度不能小于8,如果指定的数组长度小于8,则按8进行处理。
构造函数如下:
//默认长度16
public ArrayDeque() {
elements = new Object[16];
}
//如果指定长度,会调用allocateElements进行长度的归一化
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
private void allocateElements(int numElements) {
elements = new Object[calculateSize(numElements)];
}
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;
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
}
return initialCapacity;
}
初始化之后,ArrayDeque的初始结构如下:
3、数据的插入
(1)、插入数据有两个方法,addFirst(E e) 和 addLast(E e),其中
addFirst(E e):操作head指针,从头部插入(逻辑上)
addLast(E e):操作tail指针,从尾部插入(逻辑上)
3.1 addFirst(0):
addFirst(E e)源码解释:
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
说明1:代码中(head - 1) & (elements.length - 1)的作用:
进行(head - 1) & (elements.length - 1)计算之后才赋予head的原因是防止head指针的下标小于0
为了防止这种情况发生(假如数组长度elements.length = 16):
(1)、当head = 0时,head - 1 = -1,数组的下标是不能小于0的,为了防止这种情况发生,将 -1 与 15进行与运算,得到的结果是15。也就是说当head下标小于0时,将head指向到数组的最大处。
3.2 addLast(1)
addLast(E e)源码解释:
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
说明2:代码中(tail + 1) & (elements.length - 1)的作用:
进行(tail + 1) & (elements.length - 1)计算之后才赋予tail的原因是防止tail指针的下标大于数组的最大下标
为了防止这种情况发生(假如数组长度elements.length = 16):
(1)、当tail= 15时,tail + 1 = 16,数组的下标是不能大于16的,为了防止这种情况发生,将 16 与 15进行与运算,得到的结果是0。也就是说当tail下标大于数组的最大下标时,将tail指向到数组下标为0处。
4、获取数据
为了更好的说明,我们在head指针插入偶数,tail指针插入奇数,假如我们的数据结构现在如下:
获取数据有4个方法:
peekFirst():
操作head指针,获取head指针处的数据,但不移动head指针(只获取数据,不删除数据)
操作之后数据结构如下:
pollFirst():
操作head指针,获取head指针处的数据,移动head指针(获取数据,并从数组中删除数据)
操作之后数据结构如下:
peekLast():
操作tail指针,获取tail指针处的数据,但不移动tail指针(只获取数据,不删除数据)
操作之后数据结构如下:
pollLast():
操作tail指针,获取tail指针处的数据,移动tail指针(获取数据,并从数组中删除数据)
操作之后数据结构如下:
5、数组扩展
当head = tail时,表示此时数组已经塞满,需要进行扩展,扩展代码在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); //作者注:将原数组head到末尾的数据拷贝到新数组从0下标下标位置
System.arraycopy(elements, 0, a, r, p); //作者注:将原数组0到tail的数据拷贝到新数组。
elements = a;
head = 0;
tail = n;
}
下面通过直观的数据结构表现这部分代码是如何扩展数组的。
假设原来的数组长度是16,已经塞满,如下:
扩展之后的数组长度是32,数据拷贝之后形成的结构如下:
6、ArrayDeque使用场景
1、作者是在追踪Flink的barrier的处理过程中,看到了ArrayDeque的使用,在Flink中,barrier的添加和删除相当于是使用了ArrayDeque的队列功能,从tail添加,从head删除。
2、ArrayDeque 作为队列使用时性能比 LinkedList 好,作为栈使用时性能比Stack好。