目录
按照特性组织:是否并发队列(是否线程安全)、是否阻塞队列、是否有界队列
其他特殊属性:优先队列(本身数据结构不是队列而是堆)、交换队列、延迟队列
队列也是线性表的一种,其特点就是先进先出(FIFO),即从线性表的一端进从另一端出,所以也是一种操作受限的线性表。与 栈 的数据结构一样,队列可以基于数组进行实现,称为顺序队列,也可以基于链表实现,称为链式队列。先进先出特点鲜明到就是在作排队等待,所以经常用于各种资源受限的情况下排队等待(比如线程池)。个人理解受限的数据结构和方法,限制本身也觉得了其特色的使用场景,根据队列的不同形态有 队列、双端队列和环形队列。入队和出队操作,只涉及到head、tail指针的跳动,所以时间复杂度为O(1)。
1、队列的分类
按照实现分为:顺序队列、链式队列
数组在创建的时候就确定了大小,不能扩容,所以一般基于数组实现的顺序队列是有界队列;
链表可以利用碎片的内存空间,使用指针(引用)链接下一个节点,本身就是动态扩容的,所以一般基于链表实现的链式队列无界队列。比如 Java juc中的 LinkedBlockQueue默认的大小为 Integer.MAX_VALUE,可以认为是无界队列,只是我们在创建对象时可以 指定队列的长度,变成有界队列。
按照结构分为:普通队列、双端队列、环形队列(单向和双向)
根据数据结构分成明显,普通队列(单向队列)只能从一端进一端出,并且FIFO;
双端队列在管道的两端都可以入队出队;
环形队列则说明空间是一定的,当数据量无限制增大的时候不能进行扩容,则需要根据策略到达是覆盖数据还是丢弃数据。环形队列也分为单向环形队列和双向环形队列,如果队列和双端队列是使用类似管道的方式,根据可以从管道的一个或者两个方向出入;那么环形队列与head和tail指针有关,单端队列从tail处进从head处出,双端队列可以从head和tail两个点进出。 环形队列应用场景还是非常多的,比如Mysql的
按照特性组织:是否并发队列(是否线程安全)、是否阻塞队列、是否有界队列
队列的场景主要用于有限的资源进行排序等场景,我们一般理解的队列数据结构是处理单机(同一个JVM)中时,此时可能需要协调多个线程协调等待有限资源(或者临界区),所以需要队列本身支持并发。在Java中最简单的支持并发队列,就是在队列的入队(enqueue)、出队(dequeue)两个方法上添加 synchronized关键字,当然也可以基于 Unfase类的CAS机制进行实现(这里涉及了大量的并发知识,可以关注高并发系列)。
阻塞队列是指出队操作时,如果队列已经为空了,不能马上返回空,而是需要使用自旋等方式,知道队列中又添加了新的数据时,获取新的数据返回。阻塞队列主要用于生产者消费者模式。比如Java线程池中,当创建了一个线程(可能是线程池预热就创建了,也可能是基于线程池原理,添加任务时创建的),本身就是创建了一个Worker对象(该对象本身就是一个Runnable),操作系统调度会直接调用Worker的run方法,该方法会先处理因为我们添加任务时创建的任务,执行完成后就会去线程池队列中获取任务执行,而该队列我们一般使用LinkedBlockingQueue(其就是一个阻塞队列),当任务执行完队列为空时,一直阻塞知道有新的任务添加到队列。如果此时使用的不是阻塞队列,则马上返回结果,run方法执行完成后,就会进行销毁。具体可以参考ThreadPoolExecutor源码解析。
上面提到是否有界队列本身与队列的实现有关(链式队列还是顺序队列),是否有界主要表现在队列中的任务堆积时,不能在特定的时间内处理,或者造成饥饿。
其他特殊属性:优先队列(本身数据结构不是队列而是堆)、交换队列、延迟队列
优先队列本身是基于堆的数据结构实现,后面专门分析,是树的一种结构。而交换队列(Java中包含Transfer字样的队列),延迟队列直接与业务相关。
所以一个队列本身可能集上面的多个特点,这就需要我们根据业务进行选择,比如 LinkedBlockingDeque = 链式队列 + 双端队列 + 阻塞队列 + 默认无界队列 + 并发队列;队列本身比较复杂,用于各种场景需求,这里就拿Java中的队列进行映射(只是Java中没有环形队列,因为使用场景比较特殊),java中是队列分类特性:
- 队列名称中含有 Linked字样的是基于链表实现的链式队列,名称包含 Array字样的是基于数组实现的 顺序队列
- Dqueue接口继承自Queue接口,带有Dqueue字样,获取该接口的子类的,都是双端队列;否则是单向队列
- 基于BlockingQueue字样或者该接口的子类,都是阻塞队列,否则是非阻塞队列
- BlockingDqueue接口的子类,即是阻塞队列又是双端队列
2、队列的实现
1)、顺序单向队列
基于数组实现的顺序队列本身比较简单,只是需要注意队空的判断条件为 head == tail,队满的判断条件为 tail == array.length。如果要实现支持动态扩容的队列,则需要与ArrayList一样,在添加元素时判断,如果tail == array.length则要新增一个更大的数组,并将原数据拷贝过去。之前在大O复杂度表示法(最后部分)只有均摊的时间复杂度分析方法,分析了其新增的时间复杂度也为O(1)。
public class DynamicArrayQueue {
// 数组:items,数组大小:n
private String[] items;
private int n = 0;
// head表示队头下标,tail表示队尾下标
private int head = 0;
private int tail = 0;
// 申请一个大小为capacity的数组
public DynamicArrayQueue(int capacity) {
items = new String[capacity];
n = capacity;
}
// 入队操作,将item放入队尾
public boolean enqueue(String item) {
// tail == n表示队列末尾没有空间了
if (tail == n) {
// tail ==n && head==0,表示整个队列都占满了
if (head == 0) return false;
// 数据搬移
for (int i = head; i < tail; ++i) {
items[i-head] = items[i];
}
// 搬移完之后重新更新head和tail
tail -= head;
head = 0;
}
items[tail] = item;
tail++;
return true;
}
// 出队
public String dequeue() {
// 如果head == tail 表示队列为空
if (head == tail) return null;
// 为了让其他语言的同学看的更加明确,把--操作放到单独一行来写了
String ret = items[head];
++head;
return ret;
}
public void printAll() {
for (int i = head; i < tail; ++i) {
System.out.print(items[i] + " ");
}
System.out.println();
}
}
2)、环形顺序链表
环形顺序队列也是面试高频,所以需要自己能抓住重要的特点(下面的实现方式,会浪费一个数组空间,否则没有条件判断队满或者队空):
1、有两个head、tail下标分别指向队头和队尾,队尾与入队(enqueue)方法关联,队头head与出队(dequeue方法关联);
2、判断队列空的条件比较简单,head == tail;
3、判断队列满的条件是 (tail + 1)% n == head;// n为队列长度,也就是存储队列数据的数组的长度
4、入队和出队方法,先判断完是否队满或者队空后;直接根据下标获取或者存储数据;再将下标值加1,而加1操作可能使数组下标越界此时需要让其数组头。比较巧妙的技巧是使用数组长度取余,或者判断下标已经与数组长度相同则置为0
tail = (tail + 1) % queueLength; 或 tail = tail + 1 == queueLength ? 0 : tail + 1;
public class CircularQueue<T> {
/** 队列数据 */
private Object[] data;
/** 队列的长度 */
private int queueLength;
/** 队头的位置 */
private int head;
/** 队尾的位置 */
private int tail;
public static void main(String[] args) {
// 取出元素1
// 存储第四个元素返回false,因为不空一个位置的话,没有条件判断队空队满
CircularQueue<Integer> circularQueue = new CircularQueue<>(4);
// head = 0, tail = 0;
circularQueue.enqueue(1);
circularQueue.enqueue(2);
Integer dequeue = circularQueue.dequeue();
System.out.println("取出元素" + dequeue);
circularQueue.enqueue(3);
circularQueue.enqueue(4);
Boolean enqueue = circularQueue.enqueue(5);
System.out.println("存储第四个元素返回" + enqueue + ",因为不空一个位置的话,没有条件判断队空队满");
}
public CircularQueue(int queueLength) {
data = new Object[queueLength];
this.queueLength = queueLength;
}
public Boolean enqueue(T t) {
if ((tail + 1) % queueLength == head) {
return false;
}
data[tail] = t;
// tail = (tail + 1) % queueLength;
tail = tail + 1 == queueLength ? 0 : tail + 1;
return true;
}
public T dequeue() {
if (head == tail) {
return null;
}
T result = (T)data[head];
// head = (head + 1) % queueLength;
head = head + 1 == queueLength ? 0 : head + 1;
return result;
}
}