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

前言

前言点击此处查看:
http://blog.csdn.net/wang7807564/article/details/79113195

ArrayDeque

ArrayDeque的数据结构要比PriorityQueue要简单得多,是通过数组来实现的。但是,ArrayDeque的特点是一个双端队列,既可以实现FIFO的Queue,也可以实现LIFO的Stack.
ArrayDeque虽然原理比较简单,但是其精髓是使用了位运算来加快计算效率。它的源代码如下:

  1. 采用数组存储数据,数组的默认长度是16:
    /**
     * Constructs an empty array deque with an initial capacity
     * sufficient to hold 16 elements.
     */
    public ArrayDeque() {
        elements = new Object[16];
    }
  1. 存储数组长度一定是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

  1. 获取队列长度
    ArrayDeque有两个变量head,tail分别代表数组中队首和队尾的索引,由于是双端队列,实现起来类似一个循环数组,有可能队尾的索引数值比队头的数值还要小,其是通过这种方法来实现获取数组长度的:
    public int size() {
        return (tail - head) & (elements.length - 1);
    }

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

2^n - 1

计算之后的二进制结果前半部分都是0,后半部分都是1,能够起到类似掩码的作用,
例如下面的示意图:

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

  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作为掩码使用。

  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次幂数值用处很大。

  1. 数组扩容
    在插入元素之后,要进行数组是否扩容的判断,在如果需要扩容,则扩容一倍,确保存储数组的长度仍然是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;
    }

猜你喜欢

转载自blog.csdn.net/wang7807564/article/details/79636646
今日推荐