文章目录
1、描述
底层:可变的循环数组
LinkedList实现了队列
Queue
接口和双端队列Deque
,Java容器类中海油一个双端队列的实现类ArrayDeque
,它是基于数组实现的
2、实现原理
ArrayDeque
内部主要有如下实例变量:
/**
* The array in which the elements of the deque are stored.
* The capacity of the deque is the length of this array, which is
* always a power of two. The array is never allowed to become
* full, except transiently within an addX method where it is
* resized (see doubleCapacity) immediately upon becoming full,
* thus avoiding head and tail wrapping around to equal each
* other. We also guarantee that all array cells not holding
* deque elements are always null.
*/
transient Object[] elements; // 存储元素的数组
/**
* The index of the element at the head of the deque (which is the
* element that would be removed by remove() or pop()); or an
* arbitrary number equal to tail if the deque is empty.
*/
transient int head;
/**
* The index at which the next element would be added to the tail
* of the deque (via addLast(E), add(E), or push(E)).
*/
transient int tail;
ArrayDeque
的高效来源于head
和tail
这两个变量它们使得物理上简单的从头到尾的数组变为了逻辑上循环数组,避免了在头尾操作时的移动
3、构造方法
3.1、 ArrayDeque()
//分配一个长度为16的数组
public ArrayDeque() {
elements = new Object[16];
}
3.2、ArrayDeque(int numElements)
分配的数组的大小 为
分配的实际长度是严格大于numElements并且为2的整数次幂的最小数
- 如果numElements小于8,就是8
- 在numElements大于等于8的情况下:分配的实际长度是严格大于numElements并且为2的整数次幂的最小数
//不是简单的分配给定的长度,而是调用allocateElements
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
private void allocateElements(int numElements) {
int initialCapacity = MIN_INITIAL_CAPACITY;
// Find the best power of two to hold elements.
// Tests "<=" because arrays aren't kept full.
//如果numElements小于8,就是8
//在numElements大于等于8的情况下,
// 分配的实际长度是严格大于numElements并且为2的整数次幂的最小数
//比如,如果numElements为10,则实际分配32
/**
* 为什么要2的幂次数呢? 这样会使得很多操作的效率很高。
* 为什么要严格大于numElements呢?
* 因为循环数组必须至少留一个空位,tail变量指向下一个空位,为了容纳numElements个元素,至少需要numElements+1个位置
*/
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
}
elements = new Object[initialCapacity];
}
3.3、ArrayDeque(Collection<? extends E> c)
public ArrayDeque(Collection<? extends E> c) {
//同样调用allocateElments分配数组
allocateElements(c.size());
//随后调用addAll,而addAll只是循环调用了add方法
addAll(c);
}
4、尾部添加 add方法
//从尾部添加
public boolean add(E e) {
addLast(e);
return true;
}
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;//将元素添加到tail处
//将tail指向下一个位置
//如果队列满了,则调用doubleCapacity扩展数组
//tail的下一个位置是 (tail + 1) & (elements.length - 1)
//这种位操作是循环数组中的一种常见操作,效率也很高
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();//将数组扩大为2倍
}
/**
* Doubles the capacity of this deque. Call only when full, i.e.,
* when head and tail have wrapped around to become equal.
* 当head与tail相等时才会调用
* 分配一个长度翻倍的新数组a,
* 将head右边的元素复制到新数组开头处;再复制左边的元素到新数组中;
* 最后重新设置head和tail,head设置为0,tail设置为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;//将数组长度扩大2倍
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;
}
循环数组扩容前后对比
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ghykcvkS-1582179313091)(images/01.png)]
5、头部添加 addFirst方法
//在头部添加,要让head先执行前一个位置,然后再赋值给head所在位置
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
//head的前一个位置为 (head - 1) & (elements.length - 1)
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)//若数组已经满了,则扩大循环数组
doubleCapacity();
}
6、从头删除removeFirst
//从头部删除
public E removeFirst() {
E x = pollFirst();
if (x == null)
throw new NoSuchElementException();
return x;
}
public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
E result = (E) elements[h];
// Element is null if deque empty
if (result == null)
return null;
//将原头部位置为null
elements[h] = null; // Must null out slot
//将head设置为下一个位置,下一个位置为(h+1)&(elements.lengh-1)
head = (h + 1) & (elements.length - 1);
return result;
}
7、查看长度 size()
ArrayDeque没有单独的字段维护长度,其
size
方法的代码为:
//ArrayDeque没有单独的字段维护长度
public int size() {
return (tail - head) & (elements.length - 1);
}
8、检查给定元素是否存在contains
public boolean contains(Object o) {
//在ArrayDeque中,有效元素不允许为null
if (o == null)
return false;
int mask = elements.length - 1;
//从head开始遍历并进行对比,循环过程没有使用tail
//而是到元素为null就结束,这是因为在ArrayDeque中,有效元素不允许为null
int i = head;
Object x;
//遍历整个数组
while ( (x = elements[i]) != null) {
if (o.equals(x))
return true;
i = (i + 1) & mask;
}
return false;
}
9、toArray方法
public Object[] toArray() {
//转换为数组
return copyElements(new Object[size()]);
}
//拷贝元素
private <T> T[] copyElements(T[] a) {
if (head < tail) {
//如果head小于tail,就是从head开始复制size个
System.arraycopy(elements, head, a, 0, size());
} else if (head > tail) {
//否则复制逻辑与doubleCapaity方法中的类似
//先复制从head到末尾的部分,然后复制从0到tail的部分
int headPortionLen = elements.length - head;
System.arraycopy(elements, head, a, 0, headPortionLen);
System.arraycopy(elements, 0, a, headPortionLen, tail);
}
return a;
}
10、小结
ArrayDeque
内部就是一个动态扩展的循环数组,通过head
和tail
变量维护数组的开始和结尾,数组的长度为2的幂次方,使用高效的位操作进行各种判断,以及对head和tail进行维护。
·ArrayDeque特点分析:`
ArrayDeque
实现了双端队列,内部使用循环数组实现,这决定了它由如下特点:
- 在两端添加、删除元素的效率很高,动态扩展需要内存分配以及数组复制开销可以被平摊,具图来说,添加N个元素的效率为
O(N)
- 根据元素内容查找和删除的效率比较目的,为
O(N)
- 与
ArrayList
和LinkedList
不同,没有索引位置的概念,不能根据索引位置进行操作
ArrayDeque和LinkedList都实现了Deque接口,应该用哪一个呢?
- 如果只需要Deque接口,从两端进行操作,一般而言,
ArrayDeque
效率高一些,应该被优先使用; - 如果同时需要根据索引位置进行操作,或者经常需要在中间进行插入和删除,则应该选
LinkedList