这篇文章介绍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的初始结构如下:

java array的复杂度 arraydeque java_数组

3、数据的插入

(1)、插入数据有两个方法,addFirst(E e) 和 addLast(E e),其中

        addFirst(E e):操作head指针,从头部插入(逻辑上)

        addLast(E e):操作tail指针,从尾部插入(逻辑上)

3.1 addFirst(0):

java array的复杂度 arraydeque java_ci_02

 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)

java array的复杂度 arraydeque java_ci_03

 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指针插入奇数,假如我们的数据结构现在如下:

java array的复杂度 arraydeque java_ci_04

获取数据有4个方法:

peekFirst():

操作head指针,获取head指针处的数据,但不移动head指针(只获取数据,不删除数据)

操作之后数据结构如下:

java array的复杂度 arraydeque java_ci_04

pollFirst(): 

操作head指针,获取head指针处的数据,移动head指针(获取数据,并从数组中删除数据)

操作之后数据结构如下:

java array的复杂度 arraydeque java_java array的复杂度_06

peekLast():

操作tail指针,获取tail指针处的数据,但不移动tail指针(只获取数据,不删除数据)

操作之后数据结构如下:

pollLast():

操作tail指针,获取tail指针处的数据,移动tail指针(获取数据,并从数组中删除数据)

操作之后数据结构如下:

java array的复杂度 arraydeque java_java_07

 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,已经塞满,如下:

java array的复杂度 arraydeque java_数组_08

 扩展之后的数组长度是32,数据拷贝之后形成的结构如下:

java array的复杂度 arraydeque java_数据结构_09

 6、ArrayDeque使用场景

1、作者是在追踪Flink的barrier的处理过程中,看到了ArrayDeque的使用,在Flink中,barrier的添加和删除相当于是使用了ArrayDeque的队列功能,从tail添加,从head删除。

2、ArrayDeque 作为队列使用时性能比 LinkedList 好,作为栈使用时性能比Stack好。