队列(queue)是一种先入先出(FIFO)的数据结构。
栈中的插入和移除数据项的命名一般都是 Push、Pop,而队列至今没有标准化的命名。插入操作可以用 add、offer、enque 等命名,移除操作可以用 poll、remove、deque 等命令,查看队头操作可以用 peek、remove 等命令。
在 Java 中,常见的队列操作以及它们的区别如下所示:
插入 | offer | 向队列插入元素,在一个满的队列中加入一个新项会返回false |
add | 向队列插入元素,在一个满的队列中加入一个新项会抛出异常 | |
删除 | poll | 从队列中删除第一个元素,在队列为空时返回null |
remove | 从队列中删除第一个元素,在队列为空时会抛出异常 | |
查看队头元素 | peek | 在队列的头部查询元素,在队列为空时返回null |
element | 在队列的头部查询元素。在队列为空时会抛出异常 |
注意:这只是 Queue 接口中的声明规范,并不是强制要求。例如在 PriorityQueue 与 ArrayDeque 中,add 实际调用了 offer 方法,无论使用哪一种方法都会抛出异常。
优先级队列
优先级队列像普通队列一样,拥有队头和队尾,但队列中的元素是按照自然排序或者指定的比较器排序的。这一特性在实际中有很多应用,比如对于重要客户或紧急的消息可以优先处理。自 JDK 1.5 起,Java 容器类库中提供了优先级队列的工具 — PriorityQueue。
PriorityQueue 是基于优先堆的一个无界队列,不允许插入不可比较的对象,否则会抛出 ClassCastException 异常(如果队列只有一个元素则不会报错,因为此时没有比较);也不允许插入 null,否则会抛出NullPointerException。
注:PriorityQueue 是非线程安全的,如果想要在多线程的环境下使用,可以使用 PriorityBlockingQueue。
接下来编写一个使用 PriorityQueue 的例子。
Customer 表示客户,level 等级越高代表客户优先级越高。
public class Customer implements Comparable<Customer>{
private int level;
private String name;
private String info;
Customer() {}
Customer(int level) {
this.level = level;
}
Customer(int level, String name, String info) {
this.level = level;
this.name = name;
this.info = info;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public int compareTo(Customer o) {
//实现最大的元素在队头
return level > o.level ? -1 : (level == o.level ? 0 : 1);
}
}
接下来编写测试类,通过 factory() 方法生成 Customer 数组,设定随机 level 值,如果有多个元素的值相同,则从中任意选择一个。
public class PriorityQueueDemo {
private static Random random = new Random();
public static Customer[] factory(int i) {
Customer[] customers = new Customer[i];
for(int t = 0; t < i; t++) {
customers[t] = new Customer(random.nextInt(10));
}
return customers;
}
public static void main(String[] args) {
PriorityQueue priorityQueue = new PriorityQueue();
Customer[] customers = factory(10);
for(int i = 0; i < customers.length; i++)
priorityQueue.add(customers[i]);
Customer customer;
for(int j = 0; j < customers.length; j++) {
customer = (Customer) priorityQueue.remove();
System.out.println(customer.getLevel());
}
}
}
输出结果:
9
9
9
8
8
6
6
1
1
0
PriorityQueue 原理
PriorityQueue 是基于最小堆原理实现。最小堆是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节的值。完全最小堆的示意图如下(摘自维基百科):
![priority queue](https://i-blog.csdnimg.cn/blog_migrate/10540ac989a7fb12a5bbaa46c464f661.png)
完全二叉树采用数组存储。如果根节点从0开始,对于一个节点 i 来说,它的左节点为 2i + 1,右节点为 2i + 2。当插入或移除一个节点时,会调整堆结构使得插入后能满足最小堆的特征。
下面是怎样调整的一个例子(参考PriorityQueue源码):
public class MinHeap<T> {
private Object[] queue;
private int size;
public MinHeap() {
queue = new Object[11];
}
public boolean offer(T t) {
int k = size;
if (size == 0)
queue[0] = t;
size++;
moveUp(k, t);
return true;
}
//从堆底一层层上浮
public void moveUp(int k, T t) {
Comparable<? super T> key = (Comparable<? super T>) t;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((T) e) > 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
//从堆顶一层层下沉
public T poll() {
if (size == 0)
return null;
int s = --size;
T result = (T) queue[0];
T end = (T) queue[s];
queue[s] = null;
if (s != 0)
moveDown(0, end);
return result;
}
public void moveDown(int k, T end) {
Comparable<? super T> key = (Comparable<? super T>) end;
int half = size >>> 1;
while (k < half) {
int left = (k << 1) + 1;
int right = left + 1;
Object c = queue[left];
if (right < size && ((Comparable<? super T>) c).compareTo((T) queue[right]) > 0) {
c = queue[left = right];
}
if (key.compareTo((T) c) <= 0)
break;
queue[k] = c;
k = left;
}
queue[k] = key;
}
public static void main(String[] args) {
MinHeap minHeap = new MinHeap();
minHeap.offer(1);
minHeap.offer(5);
minHeap.offer(3);
minHeap.offer(2);
minHeap.offer(4);
minHeap.offer(4);
int length = minHeap.size;
for (int i = 0; i < length; i++)
System.out.println(minHeap.poll());
}
}
输出结果:
1
2
3
4
4
5
在 PriorityQueue 中,插入、删除操作的时间复杂度为 O(log(n)),查看元素的操作时间复杂度为线性时间。可以参考下面的链接: