Queue继承自 Collection,咱们先来看看类结构吧,代码量比较少,我直接贴代码了前端
public interface Queue<E> extends Collection<E> {
boolean add(E var1);
boolean offer(E var1);
E remove();
E poll();
E element();
E peek();
}复制代码
从方法名上不太好猜每一个方法的做用,咱们直接来看 API 吧算法
~ | 抛出异常 | 返回特殊值 |
---|---|---|
插入 | add(e) | offer(e) |
移除 | remove() | poll() |
检查 | element() | peek() |
好像就除了对增删查操做增长了一个不抛出异常的方法,没什么特色吧,咱们继续看描述~后端
在处理元素前用于保存元素的 collection。除了基本的 Collection 操做外,队列还提供其余的插入、提取和检查操做。每一个方法都存在两种形式:一种抛出异常(操做失败时),另外一种返回一个特殊值(null 或 false,具体取决于操做)。插入操做的后一种形式是用于专门为有容量限制的 Queue 实现设计的;在大多数实现中,插入操做不会失败。api
就描述了这三组方法的区别,那么之后我操做队列尽可能用不抛出异常的方法总行了吧。另外也没看出什么名堂,那么队列这个接口究竟是规范了什么行为?我记得队列好像是一种数据经常使用的结构,咱们来看看百度百科的定义吧数组
队列是一种特殊的线性表,特殊之处在于它只容许在表的前端(front)进行删除操做,而在表的后端(rear)进行插入操做,和栈同样,队列是一种操做受限制的线性表。进行插入操做的端称为队尾,进行删除操做的端称为队头。安全
看了百度百科的描述,才知道队列规范了集合只容许在表前端删除,在表后端插入。这不就是 FIFO 嘛~~bash
FIFO 是英语 first in first out 的缩写。先进先出,想象一下,在车辆在经过不容许超车的隧道时,是否是先进入隧道的车辆最早出隧道。数据结构
这个问题我回答不了,队列只是一种数据结构,在某些特定的场合,用队列实现效率会比较高。并发
AbstractQueue 是Queue 的抽象实现类,和Lst、Set 的抽象实现类同样,AbstractQueue 也继承自 AbstractCollection。
AbstractQueue 实现的方法很少,主要就 add、remove、element 三个方法的操做失败抛出了异常。ide
PriorityQueue 直接继承自 AbstractQueue,而且除序列号接口外,没实现任何接口,大概算是最忠诚的 Queue 实现类吧。照惯例,咱们先来看看 API 介绍。
一个基于优先级堆的无界优先级队列。优先级队列的元素按照其天然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。优先级队列不容许使用 null 元素。依靠天然顺序的优先级队列还不容许插入不可比较的对象.
此队列的头 是按指定排序方式肯定的最小 元素。若是多个元素都是最小值,则头是其中一个元素——选择方法是任意的。队列获取操做 poll、remove、peek 和 element 访问处于队列头的元素。
优先级队列是无界的,可是有一个内部容量,控制着用于存储队列元素的数组大小。它一般至少等于队列的大小。随着不断向优先级队列添加元素,其容量会自动增长。无需指定容量增长策略的细节。
进队列的数据还要进行排序,每次取都是取到元素最小值,尼玛,说好的 FIFO 呢?好吧,我暂且当这是一个取出时有顺序的队列,看起来和昨天学的 TreeSet 功能差很少哈。
PriorityQueue 叫优先队列,即优先把元素最小值存到队头。想象一下,使用PriorityQueue去管理一个班的学生,根据能够年龄、成绩、身高设置好对应的 Comparator ,而后就能自动从小到大排序呢。哈哈哈~
咱们先来看一下 PriorityQueue 的实现吧~
类成员变量以下~
public class PriorityQueue<E> extends AbstractQueue<E> implements Serializable {
private static final long serialVersionUID = -7720805057305804111L;
private static final int DEFAULT_INITIAL_CAPACITY = 11;
transient Object[] queue;
private int size;
private final Comparator<? super E> comparator;
transient int modCount;
private static final int MAX_ARRAY_SIZE = 2147483639;
}复制代码
没错,基于数组的实现,也能找到 grow 扩容方法,少了 List 的各类方法,Queue 的方法咱们前面也看了。那么咱们就以前去看他是怎么实现优先队列的~
思考一下,既然是数组实现,又能按元素大小顺序去取出,那么确定是在添加元素的时候作的排序,直接把对应的元素值大小的元素添加到对应的位置。那么咱们就从 add 方法看起吧~~
public boolean add(E var1) {
return this.offer(var1);
}
public boolean offer(E var1) {
if(var1 == null) {
throw new NullPointerException();
} else {
++this.modCount;
int var2 = this.size;
if(var2 >= this.queue.length) {
this.grow(var2 + 1);
}
this.size = var2 + 1;
if(var2 == 0) {
this.queue[0] = var1;
} else {
this.siftUp(var2, var1);
}
return true;
}
}
private void siftUp(int childIndex) {
E target = elements[childIndex];
int parentIndex;
while (childIndex > 0) {
parentIndex = (childIndex - 1) / 2;
E parent = elements[parentIndex];
if (compare(parent, target) <= 0) {
break;
}
elements[childIndex] = parent;
childIndex = parentIndex;
}
elements[childIndex] = target;
}复制代码
上面的方法调用都很简单,我就不写注释了,add 调用 offer 添加元素,若是集合里面的元素个数不为零,则调用 siftUp 方法把元素插入合适的位置。
敲黑板~~接下来的东西我看了老半天才看明白。有点吃力
注意了,siftUp里面的算法有点奇怪,我一开始还觉得是二分插入法,然而并非。
首先,咱们这里走进了一个误区,PriorityQueue 虽然是一个优先队列,可以知足咱们刚刚说的需求,把一个班的学生按年龄大小顺序取出来,可是在内存中(数组中)的保存却并非按照从小到大的顺序保存的,可是一直 poll,是可以按照元素从小到大的顺去取出结果。
这里我作了一个小测试。
PriorityQueue<Integer> integers = new PriorityQueue<>();
integers.add(8);
integers.add(6);
integers.add(5); 复制代码
已知 PriorityQueue 用数组存储,你们猜猜我这样存进队列的三个数子是怎样存储的?
一开始我觉得是五、六、8的顺序,可是 debug 的时候看到 PriorityQueue 里面保存数据数组里面的存放顺序是五、八、6.why?
而后我调用下面这个方法打印~
while (!integers.isEmpty()) {
Log.e("_____", integers.poll() + "~~");
} 复制代码
结果是五、六、8.这他妈就尴尬了。
而后怎么办~去找度娘呗。。。
好了,开始解析~~
不知道你们记不记得一种数据结构叫二叉树,这里就是使用了二叉树的思路,因此比较难理解。
首先,这里使用的是一种特殊的二叉树:1.父节点永远小于子节点,2.优先填满第 n 层树枝再填 n+1 层树枝。也就是说,数组里面的五、八、6是这样存储的
依次添加元素八、六、5.
5
/ \
8 6
‖
∨
数组角标位置
0
/ \
1 2复制代码
这样能理解了吧,再回过头去看siftUp方法,捋一下添加元素的过程。
添加8
没什么好说的,直接添加一个元素到到数组[0]便可,二叉树添加一个顶级节点
添加5
首先把[1]的位置赋值给5,使得数组中的元素为{8,5}
而后执行siftUp(1)方法(1是刚刚插入元素5的角标)
siftUp方法首先获取5的父节点,判断5是否小于父节点。
若是小于,则交换位置继续比较祖父节点
若是大于或者已经到顶级节点,结束。复制代码
siftUp方法后,数组变为{5,8}
添加6
重复上面的动做,数组变为{5,8,6}
问:若是此时添加数字7,数组的顺序是多少?
思考一下3分钟~~
好,3分钟过去了,结果是{5,7,6,8}
为何会这样?拿着数字7代入到上面的方法中去算呀,首先8在数组中的角标是3,3要去和父节点比,求父节点的公式是(3-1)/2 = 1.因而父节点的角标是1,7<8,所以交换位置,此时角标1还有父节点 (1-1)/2 = 0,再比较7和5,7>5,知足大于父节点条件,结束。
好了,如今应该明白了吧~~~没明白再回过头去理解一遍。
接下来,咱们来看循环调用 poll() 方法是怎样从{5,8,6}的数组中按照从小到大的顺序取出五、六、8.
咱们来看 poll()方法
public E poll() {
if (isEmpty()) {
return null;
}
E result = elements[0];
removeAt(0);
return result;
}
private void removeAt(int index) {
size--;
E moved = elements[size];
elements[index] = moved;
siftDown(index);
elements[size] = null;
if (moved == elements[index]) {
siftUp(index);
}
}
private void siftDown(int rootIndex) {
E target = elements[rootIndex];
int childIndex;
while ((childIndex = rootIndex * 2 + 1) < size) {
if (childIndex + 1 < size
&& compare(elements[childIndex + 1], elements[childIndex]) < 0) {
childIndex++;
}
if (compare(target, elements[childIndex]) <= 0) {
break;
}
elements[rootIndex] = elements[childIndex];
rootIndex = childIndex;
}
elements[rootIndex] = target;
}复制代码
这是 api23 里面 PriorityQueue 的方法,和 Java8 略有不一样,但实现都是同样的,只是方法看起来好理解一些。
首先 poll 方法取出了数组角标0的值,这点不用质疑,由于角标0对应二叉树的最高节点,也就是最小值。
而后在 removeAt 方法里面把数组的最后一个元素覆盖了第0个元素,再是将最后一个元素置空,好,到了这里,进入第二个关键点了,黑板敲起来~~
这里在赋值以后调用了 siftDown(0);
咱们来看 siftDown()方法~
这个方法从0角标(最顶级父节点)开始,先判断左右子节点,取较小的那个一,和父节点比较,而后再对比左右子节点。根据咱们这里二叉树的特色,最终能取到最小的那个元素放到顶级父节点,保证下一次 poll能取到当前集合最小的元素。具体代码不带着读了~~
ok,PriorityQueue 看完了。
#Deque
刚刚咱们一直在找 FIFO 的集合,找到个 PriorityQueue,然而并非。
而后咱们继续找呗,发现了 Queue 有一个子接口Deque
来看看 API 文档的定义~
一个线性 collection,支持在两端插入和移除元素。名称 deque 是“double ended queue(双端队列)”的缩写,一般读为“deck”。大多数 Deque 实现对于它们可以包含的元素数没有固定限制,但此接口既支持有容量限制的双端队列,也支持没有固定大小限制的双端队列。
此接口定义在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操做失败时抛出异常,另外一种形式返回一个特殊值(null 或 false,具体取决于操做)。插入操做的后一种形式是专为使用有容量限制的 Deque 实现设计的;在大多数实现中,插入操做不能失败。
嗯~就是一个首尾插入删除操做都直接的接口。
咱们刚刚说了 Queue 遵循 FIFO 规则,当有了 Deque,咱们还能实现 LIFO(后进先出)。反正像先进后出、后进先出都能在 Deque 的实现类上作到,具体看各位 Coder 们怎么操做了。
总结一下 Deque 的方法~
~~-- | __第一个元素(头部)..... | _最后一个元素(尾部) |
---|---|---|
~ | 抛出异常 | 特殊值 | 抛出异常 | 特殊值 |
---|---|---|---|---|
插入 | addFirst(e) | offerFirst(3) | addLast(e) | offerLast(3) |
移除 | removeFirst() | pollFirst() | removeLast() | pollLast() |
检查 | getFirst() | peekFirst() | getLast() | peekLast() |
__特么的,MD 语法不支持这种不对齐表格
若是想用做 LIFO 队列,应优先使用此接口,而不是遗留的 Stack 类。在将双端队列用做堆栈时,元素被推入双端队列的开头并从双端队列开头弹出。堆栈方法彻底等效于 Deque 方法,以下表所示:
堆栈方法 | 等效 Deque 方法 |
---|---|
push(e) | addFirst(e) |
pop() | removeFirst() |
peek() | peekFirst() |
就酱紫吧,也没什么特别的,我我的不太喜欢这个接口,我以为这个接口规范的行为有点多,不符合接口隔离原则和单一职能原则。
接下来咱们就去看看 Deque 的实现类吧。
看两个具备表明性的类吧,第一个是基于数组实现的 ArrayQeque,第二个是基于链表实现的LinkedList。
前面 List 的时候咱们看过 LinkedList,LinkedList 继承自AbstractList,同时也实现了 List 接口,所以这是一个很全能的类。一句话描述就是:基于链表结构实现的数组,同时又支持双向队列操做。
还记得以前在 List 结尾留的一个思考题么:怎样用链表的结构快速实现栈功能LinkedListStack?
public class LinkedListStack extends LinkedList{
public LinkedListStack(){
super();
}
@Override
public void push(Object o) {
super.push(o);
}
@Override
public Object pop() {
return super.pop();
}
@Override
public Object peek() {
return super.peek();
}
@Override
public boolean isEmpty() {
return super.isEmpty();
}
public int search(Object o){
return indexOf(o);
}
}复制代码
呐,这里给出了实现,其实什么都没作,就是调用了父类方法。这个类只是看起来结构清晰的实现了 LIFO,可是因为继承自 LinkedList,仍是能够调用 addFirst 等各类“非法操做方法”,这就是我说的不理解 Java 为何要这样设计,还推荐使用 Deque 替换栈实现。项目实际开发中,同窗们要使用栈结构直接用 LinkedList就好了,我这里 LinkedListStack 只是便于你们理解 LinkedList 也能够用做栈集合。
照惯例先看 API 定义~
Deque接口的大小可变数组的实现。数组双端队列没有容量限制;它们可根据须要增长以支持使用。它们不是线程安全的;在没有外部同步时,它们不支持多个线程的并发访问。禁止 null 元素。此类极可能在用做堆栈时快于 Stack,在用做队列时快于 LinkedList。
感受 ArrayDeque 才是一个正常的 Deque 实现类,ArrayDeque 直接继承自 AbstractCollection,实现了Deque接口。
类部实现和 ArrayList 同样都是基于数组,当头尾下标相等时,调用doubleCapacity()方法,执行翻倍扩容操做。
头尾操做是什么鬼?咱们都知道ArrayDeque 是双向列表,就是能够两端一块儿操做的列表。所以使用了两个指针 head 和tail 来保存当前头尾的 index,一开始默认都是0角标,当添加一个到尾的时候,tail先加1,再把值存放到 tail 角标的数组里面去。
那么 addFirst 是怎么操做的呢?head 是0,添加到-1的角标上面去?其实不是的,这里 你能够把这个数组当成是一个首尾相连的链表,head 是0的时候 addFirst 其实是把值存到了数组最后一个角标里面去了。即: 当 head 等于0的时候 head - 1 的值 数组.length - 1,代码实现以下。
如图,这是我以下代码的执行添加60时 debug
ArrayDeque<Integer> integers = new ArrayDeque<>();
integers.addLast(8);
integers.addFirst(60);复制代码
而后当head == tail的时候表示数组用满了,须要扩容,就执行doubleCapacity扩容,这里的扩容和 ArrayList 的代码差很少,就不去分析了。
凡是牵涉到须要使用 FIFO 或者 LIFO 的数据结构时,推荐使用 ArrayDeque,LinkedList 也行,还有 get(index)方法~~