您的位置:首页 > 产品设计 > UI/UE

Java高级技术第四章——Java容器类Queue之体验双端队列ArrayQueue设计之妙

2018-03-21 10:48 459 查看

前言

前言点击此处查看:

http://blog.csdn.net/wang7807564/article/details/79113195

ArrayDeque

ArrayDeque的数据结构要比PriorityQueue要简单得多,是通过数组来实现的。但是,ArrayDeque的特点是一个双端队列,既可以实现FIFO的Queue,也可以实现LIFO的Stack.

ArrayDeque虽然原理比较简单,但是其精髓是使用了位运算来加快计算效率。它的源代码如下:

采用数组存储数据,数组的默认长度是16:

/**
* Constructs an empty array deque with an initial capacity
* sufficient to hold 16 elements.
*/
public ArrayDeque() {
elements = new Object[16];
}


存储数组长度一定是2的幂指数,且最小为8,其通过这样的位运算来实现的:

/**
* The minimum capacity that we'll use for a newly created deque.
* Must be a power of 2.
*/
private static final int MIN_INITIAL_CAPACITY = 8;

// ******  Array allocation and resizing utilities ******

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;
}

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


从上面的代码可以看出来,使用calculateSize()方法可以自动计算出合适的2的n次幂的数值,用下面的代码作一个测试:

public class Main {

private static int getCapacity(int numElements) {
int 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;
}

public static void main(String[] args){
for(int i=0;i<5;i++){
System.out.println(i + " " + getCapacity(i));
}
}
}


输出结果是:

0 1

1 2

2 4

3 4

4 8

获取队列长度

ArrayDeque有两个变量head,tail分别代表数组中队首和队尾的索引,由于是双端队列,实现起来类似一个循环数组,有可能队尾的索引数值比队头的数值还要小,其是通过这种方法来实现获取数组长度的:

public int size() {
return (tail - head) & (elements.length - 1);
}


依然使用了位运算,之所以存储数组的长度都是2的n次幂的数值,是因为:

2^n - 1

计算之后的二进制结果前半部分都是0,后半部分都是1,能够起到类似掩码的作用,

例如下面的示意图:



实际上兼取绝对值与取模于一体了。

清空队列

public void clear() {
int h = head;
int t = tail;
if (h != t) { // clear all cells
head = tail = 0;
int i = h;
int mask = elements.length - 1;
do {
elements[i] = null;
i = (i + 1) & mask;
} while (i != t);
}
}


清空队列并不将数组的数组元素数量再次置为初始值,只是将数组中对每个元素的引用置为null,这样可以被垃圾回收掉。

可以看到,在遍历队列中每个元素的时候,巧妙地运用了位运算,仍然将元素数量减1作为掩码使用。

添加元素

添加元素的核心源代码如下:

public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}


判断数组容量不够时,需要进行扩容,判断方式依然是使用位运算来完成的。

对于循环数组中判断是否达到最大容量的一种方法就是判断tail与head是否相等,如果相等,那么首位相交,达到最大容量。

但是,在起始位置时候,head与tail也可能会相等。那么,可以将tail指向下一个元素将要插入的位置,那么当:

(tail + 1) % array.length == head

此时,也就是下一个将要插入的位置与head索引相同,那么证明这个数组已经被填充满了,需要扩容了。

在上面的源代码中,使用了掩码的位运算方式来代替取模的方式,效率更高。可见,数组长度是2的n次幂数值用处很大。

数组扩容

在插入元素之后,要进行数组是否扩容的判断,在如果需要扩容,则扩容一倍,确保存储数组的长度仍然是2的n次幂数值。

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);
System.arraycopy(elements, 0, a, r, p);
elements = a;
head = 0;
tail = n;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: