一、队列
1.队列基本概念
队列与栈一样是一种特殊的线性表,队列的数据元素及数据元素之间的逻辑关系与线性表一致。值得注意的是,线性类可以在任意位置执行插入或删除操作;队列只允许在一端进行插入操作,在另一端进行删除操作;区别与栈,栈所允许的插入和删除操作在同一段。
队列允许插入操作的一段称为"队尾",允许删除操作的一段称为"队头"。插入操作又称为"入队",删除操作又称为"出队"。
根据基本概念,每次入队的数据元素都放在原来的队尾之后成为新的队尾,每次出队的数据元素都是原来的队头元素。这样最先进入队列的元素总是最先出队,所以,队列又称为"先进先出表"。
2.队列结构及其特点
2.1 顺序队列
顺序队列通常由一个数组构成,将front设置为队头的索引位置,将rear设置为队尾的索引位置。其结构如下所示:
2.2 链式队列
与栈同理,由于队列只可在一端插入,一端删除,没有线性表五头节点带来不一致的问题,因此可以设置为不带头节点的链表:
3.假溢出问题与循环队列
3.1 假溢出问题
接2.1所示的图,将F,G依次入队,则:
此时,想要再插入一个元素H,却因为rear元素的值超过了数组的最大值导致越界。然而,可以明显地看到,数组其实还有空位置可以插入元素。这样的问题称为"假溢出"。
3.2 解决假溢出问题
假溢出产生的原因是完全采用物理上的头尾指针无法正确逻辑上的头尾指针,如3.1所示,按照队列的逻辑,尾指针的位置应该在数组的头部。那么如何自动构建这一关系呢,也即当头指针或尾指针到达末尾后,还继续往下走,则自动跳转到数组首位。则需要借助取余运算实现:
3.3 循环队列、判满与判空
借助取余操作可将数组变为逻辑上的循环数组,因而队列也变为逻辑上的循环队列,如下是一个空的循环队列:
然而 ,另一个问题出现了。对于循环队列,其空状态为:
满状态也为:
无法使用头尾指针的位置来分析出队列是处于满状态还是空状态。
解决方法有:
1)增设置一个
位
初始时
,入队成功
,出队成功
则队满条件为
,队空条件为
2)设置计数器
记录队列中元素数量
则队满条件为
,队空条件为
3)少使用一个元素位置
则队满条件为
,队空条件为
4.队列基本功能实现
1.顺序循环队列
其成员为头标记,尾标记,元素计数,最大容量,对象数组;方法体有出队、入队、查看队头元素、判空。
public class myArrayQueue {
static final int defaultSize = 10;
int front; //头标记
int rear; //尾标记
int count; //元素计数
int maxSize; //最大容量
Object []objArray;
public myArrayQueue(){
objArray = new Object[defaultSize];
front = rear = 0;
count = 0;
maxSize = defaultSize;
}
public myArrayQueue(int size){
objArray = new Object[size];
front = rear = 0;
count = 0;
maxSize = size;
}
public void enQueue(Object obj) throws Exception{
if (front == rear && count > 0){ //注意判满条件
throw new Exception("queue is full");
}
objArray[rear] = obj;
rear = (rear + 1)%maxSize; //构造循环队列
count++;
}
public Object deQueue() throws Exception{
if (count == 0){
throw new Exception("queue is empty");
}
Object obj = objArray[front];
front = (front + 1)%maxSize; //构造循环队列
count--;
return obj;
}
public Object getData() throws Exception{
if (count == 0){
throw new Exception("queue is empty");
}
return objArray[front];
}
public boolean isEmpty(){
return count == 0;
}
}
2.链式队列
public class myLinkedQueue {
private static class Node{
Node next;
Object data;
Node(Object obj){
data = obj;
}
}
Node front;
Node rear;
int count;
public myLinkedQueue(){
front = rear = null;
count = 0;
}
public void enQueue(Object obj){
Node p = new Node(obj);
if (rear == null){ //空链表,特殊处理
front = rear = p;
}
else { //接上新节点,修改尾指针
rear.next = p;
rear = p;
}
count++;
}
public Object deQueue() throws Exception{
if (count == 0){
throw new Exception("queue is empty");
}
Object obj = front.data;
front = front.next; //修改头指针
count--;
return obj;
}
public Object getData() throws Exception{
if (count == 0){
throw new Exception("queue is empty");
}
return front.data;
}
public boolean isEmpty(){
return count == 0;
}
}
5.性能分析
5.1 顺序循环队列入队
由于入队操作只需要一段尾标记,不需要移动大量元素,故入队时间复杂度为
5.2 顺序循环队列出队
同5.1,出队时间复杂度为
5.3 链式队列入队
链式队列有尾指针,故无需搜索,不需要移动元素,因而,链式队列入队时间复杂度为
5.4 链式队列出队
同5.3,链式队列出队时间复杂度为
5.5 综合情况
由于顺序队列存在队满的情况,若想将已满的顺序队列复制入一个更大的队列则需要花费大量的时间,因而,在不确定有多大规模的数据需要存入队列中时,采用链式表作为存储结构。
另一方面来说,链式队列存储指针域需要花费更多空间,因而在得知入队数据规模又希望节省存储空间时,采用链式队列。
6.优先级队列
6.1 什么是优先级队列
通常来说,队列满足"先进先出"。然而,有些情况,需要允许插队的存在,例如某某系统的的VIP客户,超级VIP客户,当接待客户的服务员只有一个时,超级VIP用户将优先被服务,其次是VIP用户。队列显然无法做到这一点,此时则需要优先级队列:允许给所有入队的元素赋予一个优先级标记,出队时,优先级高的元素将优先被出队。
6.2 优先级队列与普通队列的不同
1)优先级队列出队时不是把队头元素出队,而是把队列中优先级最高的元素出队。对于顺序优先级队列,还会调整在那个元素之后的所有元素,因此不会出现假溢出问题。
2)优先级队列的元素比普通队列多一个部分,也即这个元素的优先级,一般是一个整数,通常这个整数越小的元素优先级越高。
二、Queue 与 PriorityQueue
1.Queue接口
Queue是Java中关于队列的最顶级的接口,所有与队列有关类或接口均与Queue有直接或间接的关系,它继承自Collection类。
除去Collection类中的方法,它还提供入队,出队,查看队头元素的方法。它们在Queue中有两类待实现的方法,一种在操作失败是触发异常,另一种在操作失败时返回特殊值,如null或false:
操作 | 抛出异常 | 返回特殊值 |
---|---|---|
入队 | add(e) | offer(e) |
出队 | remove() | poll() |
查看队头元素 | element() | peek() |
其子类有BlockingDeque< E >, BlockingQueue< E >, TransferQueue< E >,Deque< E >,前三者均与并发有关,在此只讨论Deque< E >。Deque是支持在两端插入和删除元素的线性集合。可将其称为"双端队列",双端队列也可以用作FIFO(先进先出)队列。
Deque又有其顺序实现和链式实现。
2.顺序队列ArrayDeque和链式队列LinkedList部分源码分析
2.1 ArrayDeque入队
public boolean offer(E var1) {
return this.offerLast(var1); //调用尾部入队方法
}
public boolean offerLast(E var1) {
this.addLast(var1); //调用尾部添加方法
return true;
}
public void addLast(E var1) {
if (var1 == null) { //不能插入空元素
throw new NullPointerException();
} else {
Object[] var2 = this.elements;
var2[this.tail] = var1; //尾标记处赋值为新元素
if (this.head == (this.tail = inc(this.tail, var2.length))) { //若头标记与尾标记一致 ,则队列已满,调用扩容方法
this.grow(1); //扩容方法见https://blog.csdn.net/qq_45608306/article/details/101120948
}
}
}
2.2 ArrayDeque出队
public E poll() {
return this.pollFirst(); //调用头部出队方法
}
public E pollFirst() {
Object[] var1;
int var2;
Object var3 = elementAt(var1 = this.elements, var2 = this.head); //暂存头标记处元素
if (var3 != null) { //若其非空
var1[var2] = null; //将其赋值为空
this.head = inc(var2, var1.length); //修改头标记(循环前移一位)
}
return var3; //返回暂存的元素
}
2.3 ArrayDeque查看队头
public E peek() {
return this.peekFirst(); //调用查看队头方法
}
public E peekFirst() {
return elementAt(this.elements, this.head); //返回头标记所在位置的元素
}
2.4 LinkedList入队间接调用了linkLast,出队间接调用了unlinkFirst,其方法在数据结构之链表与Java中的LinkedList中有详细介绍。
3.PriorityQueue介绍
Java标准包中的PriorityQueue基于优先级堆的优先级队列实现。PriorityQueue元素根据其自然顺序进行排序,也可以通过在队列构建时提供的Comparator进行排序,具体取决于所使用的构造函数。给出下列关于PriorityQueue的关键信息:
3.1 优先级队列的堆结构
它采用小顶堆的逻辑结构以及数组的物理结构实现。也就是说,物理上使用数组存储元素,但逻辑上保留堆的特性,它是一个完全二叉树。
完全二叉树的性质是:设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
也就是说,存储数据元素的数组不可能出现"某个位置没有元素而其后面位置有元素"的情况。
小顶堆故名思意,就是值小的元素在顶上的堆。值得注意的是,堆结构只保证父子节点元素的大小关系,不保证兄弟节点的大小关系。也就是说,此小顶堆只能保证优先级值最小的元素在数组首部,无法保证整个数组是有序的。
而仔细观察a的下标可发现:
而小顶堆有如下性质:
也即
3.2 无界性
优先级队列是无界的,但是具有内部容量来控制用于在队列上存储元素的数组的大小。尝试将元素添加到达到某条件的优先级队列时,其容量会自动增长。
3.3 优先级队列不允许null元素
4.PriorityQueue部分源码
4.0 成员
private static final int DEFAULT_INITIAL_CAPACITY = 11; //默认大小,不传大小参数时使用
transient Object[] queue; //保存数据元素的底层数组
private int size = 0; //实际数组中非空元素数量
private final Comparator<? super E> comparator; //比较优先级使用的比较器
4.1 构造方法
关键构造方法如下,可以传入单独的构造器,单独的容量参数,单独的序列,也可以自由组合,默认容量大小为11。
当传入参数小于1时,会抛出非法值异常;传入集合"无序"(这里指的符合堆结构的顺序)时,需要先将序列堆化。
public PriorityQueue(int initialCapacity,Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
public PriorityQueue(Collection<? extends E> c) { //传入集合
if (c instanceof SortedSet<?>) { //强条件,有序集合
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
initElementsFromCollection(ss);
}
else if (c instanceof PriorityQueue<?>) { //基本条件,优先级队列
PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
initFromPriorityQueue(pq);
}
else { //否则需要堆化
this.comparator = null;
initFromCollection(c); //包含堆化函数
}
}
//也可以传入优先级队列,也可修改它的比较器
堆化部分,由于传入的序列在逻辑上不能构成一个堆,因此需要将其堆化,通过不断向下调整前 部分的元素,将序列堆化,具体堆化过程在另外的文章”数据结构之堆“中叙述。
private void initFromCollection(Collection<? extends E> c) {
initElementsFromCollection(c);
heapify();
}
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
4.2 扩容
扩容原则(1)如果容量小于64,则容量增加一倍再加2(2)如果容量不小于64且小于[整数最大值-8],则容量增加一倍 (3)如果容量大于[整数最大值-8],则容量调整为整数最大值
private void grow(int minCapacity) { //传入参数为最小容量
int oldCapacity = queue.length;
// 如果容量小于64,则容量变为2倍再加2;否则增加一半
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// 超过了[最大整数值-8]
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity); //调为最大整数值
queue = Arrays.copyOf(queue, newCapacity);
}
4.3 入队
添加可能造成数组溢出,需要扩容;添加新元素会破坏堆结构,需要重新调整堆
public boolean offer(E e) {
if (e == null) //元素判空
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length) //溢出则扩容
grow(i + 1);
size = i + 1; //维护队列元素数量统计值
if (i == 0)
queue[0] = e;
else
siftUp(i, e); //向上调整堆,将新序列堆化
return true;
}
4.4 出队
添加数组元素会破坏堆结构,需要重新调整堆
public E poll() {
if (size == 0)
return null;
int s = --size; //维护队列元素数量统计值
modCount++;
E result = (E) queue[0]; //获取堆顶元素
E x = (E) queue[s]; //获取末位元素
queue[s] = null; //末位置空
if (s != 0)
siftDown(0, x); //向下调整堆,将新序列堆化
return result;
}
4.5 查找队头元素
public E peek() {
return (size == 0) ? null : (E) queue[0]; //直接返回数组头元素
}
5.PriorityQueue性能
5.1 构建队列
自底向上建堆算法的时间复杂度为
,故构建优先级队列的时间复杂度也为
5.2 入队和出队
由于入队和出队均只需要对一个节点进行调整,而树的高度为
,故入队和出队时间复杂度均为
5.3 查看队头元素
时间复杂度为
三、Java中的其他队列
1.DelayQueue
DelayQueue 是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。
2.BlockingQueue
BlockingQueue被广泛使用在“生产者-消费者”问题中,其原因是BlockingQueue提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。
四、总结
1.队列是先进先出表,分为顺序队列和链式队列,解决顺序队列假溢出问题采用循环队列,此时需要注意队列判空可判满条件。
2.出队、入队、查看队头元素时间复杂度均为常数;由于扩容并且复制队列元素需要较多时间,因此不确定将要处于队列中的数据量时可采用链式队列。
3.优先级队列采用堆结构实现,因为堆结构可以保证优先级队列出队入队规则并且其操作的效率较高,入队和出队时间复杂度均为
4.Java中的PriorityQueue默认大小为11,扩容方式为小于64时翻倍加2,大于64时增加一半。