数据结构与算法|第六章:队列
1.项目环境
- jdk 1.8
- github 地址:https://github.com/huajiexiewenfeng/data-structure-algorithm
- 本章模块:chapter05
2.什么是队列?
可以将它想象成排队买票,先来的先买,后来的人只能站末尾,不允许插队。先进先出,这就是典型的 队列。
队列跟栈非常相似,支持的操作也很有限,最基本的操作也是两个:入队 enqueue(),放一个数据到队列尾部;出队 dequeue(),从队列头部取一个元素。
队列的应用非常广泛,比如 Java 线程池中的等待队列,JUC 中的 AQS 队列,又或者是近几年非常火爆的各类消息中间,ActiveMq、RabbitMq、Kafka 等等。
3.顺序队列
队列用数组和链表都可以实现,其中使用数组实现队列叫 顺序队列,使用链表实现的队列叫 链式队列,我们使用数组进行实现
/**
* 使用数组来实现队列
*/
public class ArrayQueue {
private String[] items;// 队列存储的元素
private int n;// 队列大小
private int head;// 头节点下标
private int tail;// 尾节点下标
private static final String[] EMPTY_ELEMENTDATA = {};
public ArrayQueue(int initialCapacity) {
if (initialCapacity > 0) {
this.items = new String[initialCapacity];
n = initialCapacity;
} else if (initialCapacity == 0) {
this.items = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: " +
initialCapacity);
}
}
/**
* 从尾部入队
*
* @param item
* @return
*/
public boolean enqueue(String item) {
if (tail == n) {
if (head == 0) {//没有出队操作
throw new RuntimeException("队列已满");
} else {//有出队操作,表示队列头部有空余空间,可以进行数据迁移
System.err.println("触发数据迁移");
for (int i = 0; i < n; i++) {
if (i <= head) {
items[i] = items[i + head];
} else {
items[i] = null;
}
}
tail = head + 1;
head = 0;
}
}
items[tail] = item;
tail++;
return true;
}
/**
* 从头部出队
*
* @return
*/
public String dequeue() {
if (tail == head) {
throw new RuntimeException("空队列");
}
String res = items[head];
items[head] = null;
head++;
return res;
}
@Override
public String toString() {
return "ArrayQueue{" +
"items=" + Arrays.toString(items) +
", n=" + n +
", head=" + head +
", tail=" + tail +
'}';
}
}
测试:
public class ArrayQueueDemo {
public static void main(String[] args) {
// 入队异常
enqueueException();
// 入队和出队
enqueueAndDequeue();
// 交替进行
alternatedenqueueAndDequeue();
}
private static void enqueueException() {
ArrayQueue arrayQueue = new ArrayQueue(5);
for (int i = 0; i < 5; i++) {
arrayQueue.enqueue("元素" + i);
System.err.println(arrayQueue.toString());
}
}
private static void enqueueAndDequeue() {
ArrayQueue arrayQueue = new ArrayQueue(5);
for (int i = 0; i < 5; i++) {
arrayQueue.enqueue("元素" + i);
System.err.println(arrayQueue.toString());
}
for (int i = 0; i < 5; i++) {
String item = arrayQueue.dequeue();
System.err.println("出队元素:"+item);
System.err.println(arrayQueue.toString());
}
}
private static void alternatedenqueueAndDequeue() {
ArrayQueue arrayQueue = new ArrayQueue(5);
for (int i = 0; i < 5; i++) {
arrayQueue.enqueue("元素" + i);
System.err.println(arrayQueue.toString());
}
for (int i = 0; i < 2; i++) {
String item = arrayQueue.dequeue();
System.err.println("出队元素:"+item);
System.err.println(arrayQueue.toString());
}
for (int i = 0; i < 2; i++) {
arrayQueue.enqueue("再次入队元素" + i);
System.err.println(arrayQueue.toString());
}
}
}
三个方法需要分开执行
enqueueException 执行结果:
ArrayQueue{items=[元素0, null, null, null, null], n=5, head=0, tail=1}
ArrayQueue{items=[元素0, 元素1, null, null, null], n=5, head=0, tail=2}
ArrayQueue{items=[元素0, 元素1, 元素2, null, null], n=5, head=0, tail=3}
ArrayQueue{items=[元素0, 元素1, 元素2, 元素3, null], n=5, head=0, tail=4}
ArrayQueue{items=[元素0, 元素1, 元素2, 元素3, 元素4], n=5, head=0, tail=5}
Exception in thread "main" java.lang.RuntimeException: 队列已满
at com.huajie.chapter05.ArrayQueue.enqueue(ArrayQueue.java:38)
at com.huajie.chapter05.ArrayQueueDemo.enqueueException(ArrayQueueDemo.java:19)
at com.huajie.chapter05.ArrayQueueDemo.main(ArrayQueueDemo.java:9)
enqueueAndDequeue 执行结果:
ArrayQueue{items=[元素0, null, null, null, null], n=5, head=0, tail=1}
ArrayQueue{items=[元素0, 元素1, null, null, null], n=5, head=0, tail=2}
ArrayQueue{items=[元素0, 元素1, 元素2, null, null], n=5, head=0, tail=3}
ArrayQueue{items=[元素0, 元素1, 元素2, 元素3, null], n=5, head=0, tail=4}
ArrayQueue{items=[元素0, 元素1, 元素2, 元素3, 元素4], n=5, head=0, tail=5}
出队元素:元素0
ArrayQueue{items=[null, 元素1, 元素2, 元素3, 元素4], n=5, head=1, tail=5}
出队元素:元素1
ArrayQueue{items=[null, null, 元素2, 元素3, 元素4], n=5, head=2, tail=5}
出队元素:元素2
ArrayQueue{items=[null, null, null, 元素3, 元素4], n=5, head=3, tail=5}
出队元素:元素3
ArrayQueue{items=[null, null, null, null, 元素4], n=5, head=4, tail=5}
出队元素:元素4
ArrayQueue{items=[null, null, null, null, null], n=5, head=5, tail=5}
alternatedenqueueAndDequeue 执行结果:
ArrayQueue{items=[元素0, null, null, null, null], n=5, head=0, tail=1}
ArrayQueue{items=[元素0, 元素1, null, null, null], n=5, head=0, tail=2}
ArrayQueue{items=[元素0, 元素1, 元素2, null, null], n=5, head=0, tail=3}
ArrayQueue{items=[元素0, 元素1, 元素2, 元素3, null], n=5, head=0, tail=4}
ArrayQueue{items=[元素0, 元素1, 元素2, 元素3, 元素4], n=5, head=0, tail=5}
出队元素:元素0
ArrayQueue{items=[null, 元素1, 元素2, 元素3, 元素4], n=5, head=1, tail=5}
出队元素:元素1
ArrayQueue{items=[null, null, 元素2, 元素3, 元素4], n=5, head=2, tail=5}
触发数据迁移
ArrayQueue{items=[元素2, 元素3, 元素4, 再次入队元素0, null], n=5, head=0, tail=4}
ArrayQueue{items=[元素2, 元素3, 元素4, 再次入队元素0, 再次入队元素1], n=5, head=0, tail=5}
前面两种就不做说明了,就是正常的出队和入队操作,第三种触发了数据迁移的操作,我们需要画图来进行演示,有助于理解
第一步,5 个元素依次入队
第二步,2 个元素出队
第三步,再次入队 2 个元素
由于队列尾部已经无法再插入,要满足先进先出的特点,所以在头部 1,2 两个位置也不能插入,触发数据迁移操作,将 3、4、5 三个元素迁移到数组下标 [1,2,3] 的位置上
再正常入队两个元素
复杂度分析:
-
出队的时间复杂度 O(1)
-
入队的时间复杂度
- 在不触发数据迁移的情况下是 O(1)
- 在触发数据迁移的情况下,均摊时间复杂度是 O(1),这种情况下使用 均摊时间复杂度 更为合理
4.链式队列
示例中的 toArray 方法主要是为了打印方便和队列操作逻辑无关
/**
* 使用链表来实现队列
*/
public class LinkedListQueue {
private Node head;// 头节点
private Node tail;// 尾节点
private int size;
public LinkedListQueue() {
}
/**
* 从尾部入队
*
* @param element
* @return
*/
public boolean enqueue(String element) {
if (head == null) {
head = tail = new Node(element, null);
} else {
Node node = new Node(element, null);
tail.next = node;
tail = tail.next;
}
size++;
return true;
}
/**
* 从头部出队
*
* @return
*/
public String dequeue() {
String res = head.item;
head = head.next;
size--;
return res;
}
private static class Node {
String item;
Node next;
Node(String element, Node next) {
this.item = element;
this.next = next;
}
}
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node x = head; x != null; x = x.next)
result[i++] = x.item;
return result;
}
@Override
public String toString() {
return "LinkedListQueue{" +
"元素集合=" + Arrays.toString(toArray()) +
'}';
}
}
测试
public class LinkedListQueueDemo {
public static void main(String[] args) {
LinkedListQueue linkedListQueue = new LinkedListQueue();
for (int i = 0; i < 5; i++) {
linkedListQueue.enqueue("元素" + i);
System.out.println(linkedListQueue.toString());
}
System.out.println("====出队两个元素====");
for (int i = 0; i < 2; i++) {
linkedListQueue.dequeue();
System.out.println(linkedListQueue.toString());
}
System.out.println("====再次入队三个元素====");
for (int i = 0; i < 3; i++) {
linkedListQueue.enqueue("再出入队元素" + i);
System.out.println(linkedListQueue.toString());
}
}
}
执行结果:
LinkedListQueue{元素集合=[元素0]}
LinkedListQueue{元素集合=[元素0, 元素1]}
LinkedListQueue{元素集合=[元素0, 元素1, 元素2]}
LinkedListQueue{元素集合=[元素0, 元素1, 元素2, 元素3]}
LinkedListQueue{元素集合=[元素0, 元素1, 元素2, 元素3, 元素4]}
====出队两个元素====
LinkedListQueue{元素集合=[元素1, 元素2, 元素3, 元素4]}
LinkedListQueue{元素集合=[元素2, 元素3, 元素4]}
====再次入队三个元素====
LinkedListQueue{元素集合=[元素2, 元素3, 元素4, 再出入队元素0]}
LinkedListQueue{元素集合=[元素2, 元素3, 元素4, 再出入队元素0, 再出入队元素1]}
LinkedListQueue{元素集合=[元素2, 元素3, 元素4, 再出入队元素0, 再出入队元素1, 再出入队元素2]}
链表的操作相对数组更加简单,只需要操作头节点和尾节点即可,而且链表没有大小限制,不需要扩容。
复杂度分析:
-
出队的时间复杂度 O(1)
-
入队的时间复杂度 O(1)
5.循环队列
上面顺序队列的实现,如果不采用数据迁移的操作,也可以使用循环队列的方式解决,同样实现队列先进先出的效果。
假设,此时数组位置 [1,2] 的元素已经出队,如果元素 1 入队,按原逻辑,tail++,但是 tail 已经和数组长度 n 相等,无法再加 1 了。
我们可以设置,tail 位置为空闲的位置 1,并将元素 1 放到数组角标 0 的位置
如果元素 2 入队,设置 tail 位置为空闲的位置 2,并将元素 2 放到数组角标 1 的位置
出队的时候,同样还是出队 head 头部的元素,这就是循环队列的思想。
代码实现:
- 难点1:需要判断在队列满时,如果存在空闲位置,tail 由 5 变成 1 的逻辑(循环思想)
- 难点2:同样 head 如何实现循环效果
以下实现可能有 bug,只在本测试用例中没有问题,主要是思路
/**
* 使用数组来实现循环队列
*/
public class CycleArrayQueue {
private String[] items;// 队列存储的元素
private int n;// 队列大小
private int count;// 实际元素个数
private int head;// 头节点下标
private int tail;// 尾节点下标
private static final String[] EMPTY_ELEMENTDATA = {};
public CycleArrayQueue(int initialCapacity) {
if (initialCapacity > 0) {
this.items = new String[initialCapacity];
n = initialCapacity;
} else if (initialCapacity == 0) {
this.items = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: " +
initialCapacity);
}
}
/**
* 从尾部入队
*
* @param item
* @return
*/
public boolean enqueue(String item) {
if (count == n) {
throw new RuntimeException("队列已满");
}
if (tail == n) {// 需要将新加入的元素添加到前面空闲节点
tail = 1;
items[0] = item;
} else {
items[tail] = item;
tail++;
}
count++;
return true;
}
/**
* 从头部出队
*
* @return
*/
public String dequeue() {
if (count == 0) {
throw new RuntimeException("空队列");
}
String res = items[head];
items[head] =null;
head = (head + 1) % n;
count--;
return res;
}
@Override
public String toString() {
return "CycleArrayQueue{" +
"items=" + Arrays.toString(items) +
", n=" + n +
", count=" + count +
", head=" + head +
", tail=" + tail +
'}';
}
}
测试
public class CycleArrayQueueDemo {
public static void main(String[] args) {
alternatedenqueueAndDequeue();
}
private static void alternatedenqueueAndDequeue() {
CycleArrayQueue arrayQueue = new CycleArrayQueue(5);
for (int i = 0; i < 5; i++) {
arrayQueue.enqueue("元素" + i);
System.err.println(arrayQueue.toString());
}
for (int i = 0; i < 3; i++) {
String item = arrayQueue.dequeue();
System.err.println("出队元素:"+item);
System.err.println(arrayQueue.toString());
}
for (int i = 0; i < 3; i++) {
arrayQueue.enqueue("第二次入队元素" + i);
System.err.println(arrayQueue.toString());
}
System.err.println("===================");
for (int i = 0; i < 5; i++) {
String item = arrayQueue.dequeue();
System.err.println("第二次出队元素:"+item);
System.err.println(arrayQueue.toString());
}
for (int i = 0; i < 5; i++) {
arrayQueue.enqueue("第三次入队元素" + i);
System.err.println(arrayQueue.toString());
}
}
}
执行结果:
CycleArrayQueue{items=[元素0, null, null, null, null], n=5, count=1, head=0, tail=1}
CycleArrayQueue{items=[元素0, 元素1, null, null, null], n=5, count=2, head=0, tail=2}
CycleArrayQueue{items=[元素0, 元素1, 元素2, null, null], n=5, count=3, head=0, tail=3}
CycleArrayQueue{items=[元素0, 元素1, 元素2, 元素3, null], n=5, count=4, head=0, tail=4}
CycleArrayQueue{items=[元素0, 元素1, 元素2, 元素3, 元素4], n=5, count=5, head=0, tail=5}
出队元素:元素0
CycleArrayQueue{items=[null, 元素1, 元素2, 元素3, 元素4], n=5, count=4, head=1, tail=5}
出队元素:元素1
CycleArrayQueue{items=[null, null, 元素2, 元素3, 元素4], n=5, count=3, head=2, tail=5}
出队元素:元素2
CycleArrayQueue{items=[null, null, null, 元素3, 元素4], n=5, count=2, head=3, tail=5}
CycleArrayQueue{items=[第二次入队元素0, null, null, 元素3, 元素4], n=5, count=3, head=3, tail=1}
CycleArrayQueue{items=[第二次入队元素0, 第二次入队元素1, null, 元素3, 元素4], n=5, count=4, head=3, tail=2}
CycleArrayQueue{items=[第二次入队元素0, 第二次入队元素1, 第二次入队元素2, 元素3, 元素4], n=5, count=5, head=3, tail=3}
===================
第二次出队元素:元素3
CycleArrayQueue{items=[第二次入队元素0, 第二次入队元素1, 第二次入队元素2, null, 元素4], n=5, count=4, head=4, tail=3}
第二次出队元素:元素4
CycleArrayQueue{items=[第二次入队元素0, 第二次入队元素1, 第二次入队元素2, null, null], n=5, count=3, head=0, tail=3}
第二次出队元素:第二次入队元素0
CycleArrayQueue{items=[null, 第二次入队元素1, 第二次入队元素2, null, null], n=5, count=2, head=1, tail=3}
第二次出队元素:第二次入队元素1
CycleArrayQueue{items=[null, null, 第二次入队元素2, null, null], n=5, count=1, head=2, tail=3}
第二次出队元素:第二次入队元素2
CycleArrayQueue{items=[null, null, null, null, null], n=5, count=0, head=3, tail=3}
CycleArrayQueue{items=[null, null, null, 第三次入队元素0, null], n=5, count=1, head=3, tail=4}
CycleArrayQueue{items=[null, null, null, 第三次入队元素0, 第三次入队元素1], n=5, count=2, head=3, tail=5}
CycleArrayQueue{items=[第三次入队元素2, null, null, 第三次入队元素0, 第三次入队元素1], n=5, count=3, head=3, tail=1}
CycleArrayQueue{items=[第三次入队元素2, 第三次入队元素3, null, 第三次入队元素0, 第三次入队元素1], n=5, count=4, head=3, tail=2}
CycleArrayQueue{items=[第三次入队元素2, 第三次入队元素3, 第三次入队元素4, 第三次入队元素0, 第三次入队元素1], n=5, count=5, head=3, tail=3}
6.阻塞队列
阻塞队列其实就是在队列基础上增加了阻塞操作。简单来说,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。
ArrayBlockingQueue 示例
-
定义一个长度为 10 的阻塞队列
-
往队列中添加(入队) 100 个元素
-
每 1 秒取出(出队)一个元素
public class BlockingDemo {
ArrayBlockingQueue<String> ab = new ArrayBlockingQueue(10);
{
init();
}
public void init() {
new Thread(() -> {
while (true) {
try {
String data = ab.take();
System.out.println("receive:" + data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
public void addData(String data) throws InterruptedException {
ab.add(data);
System.out.println("send:" + data);
Thread.sleep(1000);
}
public static void main(String[] args) throws InterruptedException {
BlockingDemo demo = new BlockingDemo();
for (int i = 0; i < 100; i++) {
demo.addData("haha" + i);
}
}
}
执行结果:
send:haha0
receive:haha0
send:haha1
receive:haha1
send:haha2
receive:haha2
send:haha3
receive:haha3
...
入队操作相关源码
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;// 重入锁
lock.lock();
try {
if (count == items.length)
return false;
else {
// 入队
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
这里使用了 ReentrantLock 进行加锁,Condition 进行线程阻塞和唤醒,enqueue
就是典型的入队操作,Java 相关的细节我们就不展开了。
出队相关源码
- notEmpty.await(); 表示通过
take()
获取队列元素时,如果 count 为 0 表示队列为空,那么通过notEmpty.await();
进行阻塞,notEmpty 是 Condition 类的实例 - 如果 count 不为 0,则通过
dequeue
出队操作
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();// 可以被中断的锁
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
7.小结
队列最大的特点就是先进先出(FIFO),主要的两个操作是入队和出队。既可以用数组来实现,也可以用链表来实现;用数组实现的叫 顺序队列,用链表实现的叫 链式队列。循环队列主要是学习思路,加深队列的理解和相关操作;最后简单的了解 Java 中基于数组实现阻塞队列的使用,其实 Java JUC 中还有其他几种队列的实现,有兴趣可以了解。
8.参考
- 极客时间 -《数据结构与算法之美》王争