赞
踩
这篇文章介绍java的数据结构之双端队列之ArrayDeque
ArrayDeque是一个双端数组,既可以当做栈使用,也可以当做队列使用。
ArrayDeque有两个指针head和tail分别指向数据开头和数据结尾,当你从一端进行数据的插入和弹出时,它可以当做栈使用;当你从一端进行数据的插入,从另一端进行数据的弹出时,它可以当做队列使用;当然也可以从两端同时进行数据的插入和弹出,这种情况就比较灵活了,具体在使用在什么场景下,目前还不太清楚,带我仔细研究后再加说明。
本篇文章从源代码的角度讨论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的初始结构如下:
(1)、插入数据有两个方法,addFirst(E e) 和 addLast(E e),其中
addFirst(E e):操作head指针,从头部插入(逻辑上)
addLast(E e):操作tail指针,从尾部插入(逻辑上)
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指向到数组的最大处。
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处。
为了更好的说明,我们在head指针插入偶数,tail指针插入奇数,假如我们的数据结构现在如下:
获取数据有4个方法:
操作head指针,获取head指针处的数据,但不移动head指针(只获取数据,不删除数据)
操作之后数据结构如下:
操作head指针,获取head指针处的数据,移动head指针(获取数据,并从数组中删除数据)
操作之后数据结构如下:
操作tail指针,获取tail指针处的数据,但不移动tail指针(只获取数据,不删除数据)
操作之后数据结构如下:
操作tail指针,获取tail指针处的数据,移动tail指针(获取数据,并从数组中删除数据)
操作之后数据结构如下:
当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,数据拷贝之后形成的结构如下:
1、作者是在追踪Flink的barrier的处理过程中,看到了ArrayDeque的使用,在Flink中,barrier的添加和删除相当于是使用了ArrayDeque的队列功能,从tail添加,从head删除。
2、ArrayDeque 作为队列使用时性能比 LinkedList 好,作为栈使用时性能比Stack好。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。