Deque
接口,双端队列,继承Queue接口,并新增了扩展方法。
Deque与Queue的区别在于,Deque会允许从队列的两端都可以添加、获取数据。
头部操作的方法有:
- getFirst、addFirst、offerFirst、peekFirst、pollFirst、removeFirst;
尾部操作的方法有:
- getLast、addLast、offerLast、peekLast、pollLast、removeLast;
ArrayDeque
继承AbstractCollection,实现Deque接口。
底层是个数组,初始默认数组长度是16;也可以在构造方法参数里指定一个队列元素个数。使用该个数根据复杂计算得到数组的长度。
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;
}
双端队列,需要有两个指针,在ArrayDeque里为两个索引位置,分别来指向数组里剩余元素的头部和尾部
/**
* 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;
/**
* 尾部添加数据时,数据被放入的索引位置 (via addLast(E), add(E), or push(E)).
*/
transient int tail;
添加数据(要求数据不能为null)的方法,add,也是尾部添加。
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (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);
System.arraycopy(elements, 0, a, r, p);
elements = a;
head = 0;
tail = n;
}
尾部添加数据代码逻辑
- 直接把数据放在tail所代表的索引位置;
- tail值 加1后与当前数组的长度减一(最后一个索引位)进行 与运算 (&),得到的结果作为新的tail值,如果此时与head相等(可以看作head指向到数组最后面了),就对数组做2倍扩容。
- 新数组容量是当前数组的2倍,同时把当前旧数组head位置到数组最后位置的所有元素copy到新数组里,从新数组的索引位置0开始放置元素。
- 把旧数组从索引0开始到tail位置的元素copy到新数组,新数组的索引起始位置是第3步放的数组的结尾位置。
- 重置head=0,tail=旧数组长度
解释代码逻辑的原理:
假设通过 new ArrayDeque() 创建一个双端队列,那么它默认的底层数组长度是16,最后一个索引位就是15。此时head和tail都指向了数组的索引0号位。
当添加元素时,会从0号位开始,同时tail指向根据与运算会一直累加。当tail =15时,新添加的元素就会放在15号位,也就是数组的最右边位置。按照计算公式 (15 +1 ) & 15=0,此时会让tail指向0号位。
在队列添加元素的时,如果头部元素没被消费,那么此时head也就是0,此时0号位说明是有值的,元素不能覆盖,所以要在下次添加元素时进行扩容。
如果头部元素有被消费,那么head数值就会累加,此时head指向的数组左边索引位都是空值,允许放新的元素。所以tail的指向会从0号位开始。直到遇到head后,再次进行数组扩容。
头部添加数据
如果说尾部添加数据,是tail从索引低位到高位的循环移动,那么头部添加数据就可以看作是head从高位向地位移动。head索引位的移动规则是 head -1 结果 与数组最后一个索引位进行 与运算。
例如给0号位添加元素后,head的下次指向就是 (0-1) & 15 = 15,直到 head与tail相等时,进行数组扩容。
小结:
- head和tail的关系可以理解为双指针在一个数组上进行左右移动,指针相遇就把数组扩容。
- 扩容两倍,是为了让与运算方便,head和tail得到的结果都是顺序性的。在构造方法里指定元素个数时,通过的复杂计算得到的就是比输入参数大的,刚好是2的次方的某值,例如输入15得到16,输入16得到32。
- 队列里的元素个数,就是head 和 tail 指向的两个索引位之间的元素个数
- 清空队列,不仅要把数组每个索引位置为null,还要更改 head = tail = 0;
AbstractSequentialList
抽象类,继承AbstractList。用于LinkedList类方法实现的模板类。
相关操作都是针对抽象方法listIterator查到元素后,再进行后续操作。
LinkedList
继承AbstractSequentialList,实现List、Deque接口。支持头、尾和随机索引位置数据的添加和获取
层使用链表的方式实现,会把链表的头部和尾部先准备好,每个链表的节点都有个双向指针。
/**
* 记录元素个数
*/
transient int size = 0;
/**
* 链表的头结点
*/
transient Node<E> first;
/**
* 链表的尾节点
*/
transient Node<E> last;
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
添加数据
不管是头部添加和尾部添加,都会把数据封装在Node对象里,通过first 、 last 进行数据的指向。
如果要根据索引位置添加数据,那么就需要在链表里先找到这个索引对应的位置,返回此处的Node对象
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
将数据封装的Node对象插入到查询返回的Node对象的前面。
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
获取数据
list会通过指定的索引来获取数据,在获取数据的时候,也是要通过上述的遍历,找到该索引位置的Node,返回封装的数据。
移除数据
在对LinkedList下的数据直接移除,或者获取了数据后移除,起始就是将first、last的指向进行了变更,被移除的Node,不再属于链表的一部分,同时为了保证能被垃圾回收器回收,将被移除的Node设置为了Null。