队列是一种重要的线性数据结构,遵循 "先进先出"(FIFO,First In First Out)的原则,即最早进入队列的元素最先被取出。队列广泛应用于调度系统、缓冲处理、广度优先搜索等场景。在算法中,巧妙地利用队列这种数据结构可以很有效地解决问题,在实际的工程应用中,有很多的技术也用到了队列这种数据结构。
一、队列的基本概念
- **队头(Front):**队列中第一个元素的位置,是元素出队的一端。
- **队尾(Rear):**队列中最后一个元素的位置,是元素入队的一端。
- 基本操作:
- 入队(Enqueue):在队尾添加元素。
- 出队(Dequeue):从队头移除元素。
- 查看队头(Peek/Front):返回队头元素(不删除)。
- 判空(IsEmpty):检查队列是否为空。
- 求长度(Size):返回队列中元素的个数。
二、队列的实现方式
队列的实现通常基于数组或链表,两种方式各有优劣:
1. 基于数组的队列(顺序队列)
- 原理 :用固定大小的数组存储元素,通过两个指针(
front和rear)分别指向队头和队尾。 - 问题 :随着元素的入队和出队,
front和rear会逐渐后移,最终导致数组 "假溢出"(即数组末尾有空间,但因rear已达边界无法入队)。 - 优化 :采用循环队列 (环形队列),将数组首尾相连,当
rear到达数组末尾时,若前方有空位则绕回头部,解决假溢出问题。 - 循环队列的关键操作:
- 入队:
rear = (rear + 1) % 容量 - 出队:
front = (front + 1) % 容量 - 判空:front == rear
- 判满:(rear + 1) % 容量 == front(这里牺牲一个位置区分空和满)
代码实现(循环队列):
java
public class CircularQueue {
private int[] queue; // 存储元素的数组
private int front; // 队头指针(指向第一个元素)
private int rear; // 队尾指针(指向最后一个元素的下一位)
private int capacity; // 队列容量
private int size; // 当前元素数量
// 初始化队列
public CircularQueue(int capacity) {
this.capacity = capacity;
queue = new int[capacity];
front = 0;
rear = 0;
size = 0;
}
// 判断队列是否为空
public boolean isEmpty() {
return size == 0;
}
// 判断队列是否已满
public boolean isFull() {
return size == capacity;
}
// 入队操作(队尾添加元素)
public void enqueue(int item) {
if (isFull()) {
throw new RuntimeException("队列已满,无法入队");
}
queue[rear] = item;
rear = (rear + 1) % capacity; // 队尾指针循环后移
size++;
}
// 出队操作(队头移除元素)
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("队列为空,无法出队");
}
int item = queue[front];
front = (front + 1) % capacity; // 队头指针循环后移
size--;
return item;
}
// 查看队头元素(不删除)
public int peek() {
if (isEmpty()) {
throw new RuntimeException("队列为空,无元素");
}
return queue[front];
}
// 获取队列当前元素数量
public int size() {
return size;
}
// 测试示例
public static void main(String[] args) {
CircularQueue queue = new CircularQueue(3);
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
System.out.println(queue.dequeue()); // 输出:1
queue.enqueue(4);
System.out.println(queue.peek()); // 输出:2
System.out.println(queue.size()); // 输出:3
}
}
2. 基于链表的队列(链式队列)
- 原理:用链表存储元素,队头指向链表头节点,队尾指向链表尾节点。
- 优势:无需预先指定容量,动态扩容,避免溢出问题,实现简单。
- 劣势:相比数组,链表的指针操作会带来额外的内存开销。
代码实现(基于链表):
java
// 链表节点类
class Node {
int data; // 节点数据
Node next; // 指向下一个节点的指针
public Node(int data) {
this.data = data;
this.next = null;
}
}
// 链式队列实现
public class LinkedQueue {
private Node front; // 队头指针(指向第一个节点)
private Node rear; // 队尾指针(指向最后一个节点)
private int size; // 当前元素数量
// 初始化队列
public LinkedQueue() {
front = null;
rear = null;
size = 0;
}
// 判断队列是否为空
public boolean isEmpty() {
return size == 0;
}
// 入队操作(队尾添加元素)
public void enqueue(int item) {
Node newNode = new Node(item);
if (isEmpty()) {
// 空队列时,队头和队尾指向同一个节点
front = newNode;
rear = newNode;
} else {
// 非空队列时,新节点接在队尾,更新队尾指针
rear.next = newNode;
rear = newNode;
}
size++;
}
// 出队操作(队头移除元素)
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("队列为空,无法出队");
}
int item = front.data;
front = front.next; // 队头指针后移
size--;
// 若队列清空,需将队尾指针也置空
if (isEmpty()) {
rear = null;
}
return item;
}
// 查看队头元素(不删除)
public int peek() {
if (isEmpty()) {
throw new RuntimeException("队列为空,无元素");
}
return front.data;
}
// 获取队列当前元素数量
public int size() {
return size;
}
// 测试示例
public static void main(String[] args) {
LinkedQueue queue = new LinkedQueue();
queue.enqueue(10);
queue.enqueue(20);
queue.enqueue(30);
System.out.println(queue.dequeue()); // 输出:10
System.out.println(queue.peek()); // 输出:20
System.out.println(queue.size()); // 输出:2
}
}
三、Java中队列的实现
由于作者是一位Java后端开发人员,所以这里主要以Java中队列的实现形式来详细介绍队列的扩展功能。
在Java 中主要通过java.util.Queue接口规范了队列的基本操作,并提供了多种实现类,适用于不同场景。
Java 中的 Deque(Double-Ended Queue的缩写) 接口是双端队列,它继承自 Queue 接口,其实现主要有 ArrayDeque 、 LinkedList 、ConcurrentLinkedDeque、BlockingDeque(接口)。
一、Queue 接口中核心方法
Queue 接口是继承自 Collection 接口,有如下的队列核心操作:
|----------|-------------|----------------|---------------------------------|
| 操作类型 | 方法 | 说明 | 异常情况 |
| 入队 | add(e) | 在队尾添加元素 | 队列满时抛出IllegalStateException |
| 入队 | offer(e) | 在队尾添加元素(推荐) | 队列满时返回false |
| 出队 | remove() | 移除并返回队头元素 | 队列为空时抛出NoSuchElementException |
| 出队 | poll() | 移除并返回队头元素(推荐) | 队列为空时返回null |
| 查看队头 | element() | 返回队头元素(不移除) | 队列为空时抛出NoSuchElementException |
| 查看队头 | peek() | 返回队头元素(不移除,推荐) | 队列为空时返回null |
二、Queue 的主要实现类
1.LinkedList(链表队列)
- 底层结构:双向链表。
- 特点:
- 实现了
Queue和Deque接口,可作为队列或双端队列使用。 - 无容量限制(理论上受内存限制),不会出现队列满的情况。
- 入队 / 出队操作效率为
O(1),但因链表节点的内存开销,性能略低于数组实现。 - 适用场景:需要动态扩容、不确定元素数量的场景。
java
Queue<Integer> queue = new LinkedList<>();
queue.offer(1); // 入队
queue.offer(2);
System.out.println(queue.poll()); // 出队:1
System.out.println(queue.peek()); // 查看队头:2
2.ArrayDeque(双端队列)
- 底层结构:动态扩容的循环数组。
- 特点:
- 实现了
Deque接口,支持两端入队 / 出队(兼具队列和栈的功能)。 - 初始容量为 8,当元素满时自动扩容(翻倍)。
- 入队 / 出队操作效率为
O(1),数组结构减少了链表的指针开销,性能优于LinkedList。 - 适用场景:需要高效双端操作、频繁入队出队的场景(推荐作为队列 / 栈的首选)。
java
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(10);
queue.offer(20);
System.out.println(queue.poll()); // 10
3.PriorityQueue(优先级队列)
- 底层结构:基于二叉堆(小顶堆)。
- 特点:
- 不遵循 FIFO,而是按元素优先级排序(默认自然顺序,可自定义
Comparator)。 - 队头始终是优先级最高的元素(如最小元素)。
- 入队操作
O(log n),出队操作O(log n)。 - 适用场景:需要按优先级处理任务的场景(如调度系统)
java
// 默认小顶堆(元素从小到大出队)
Queue<Integer> pq = new PriorityQueue<>();
pq.offer(3);
pq.offer(1);
pq.offer(2);
System.out.println(pq.poll()); // 1(最小元素)
System.out.println(pq.poll()); // 2
// 自定义大顶堆(元素从大到小出队)
Queue<Integer> maxPq = new PriorityQueue<>((a, b) -> b - a);
maxPq.offer(3);
maxPq.offer(1);
maxPq.offer(2);
System.out.println(maxPq.poll()); // 3(最大元素)
4.BlockingQueue(阻塞队列)
BlockingQueue 是继承自 Queue 的子接口,增加了阻塞操作 (当队列满 / 空时,线程会阻塞等待),专为多线程协作设计。
主要实现类有:
|-------------------------|---------------------------------------------|
| 实现类 | 特点 |
| ArrayBlockingQueue | 基于数组的有界阻塞队列,容量固定,支持公平 / 非公平锁。 |
| LinkedBlockingQueue | 基于链表的阻塞队列,默认容量为Integer.MAX_VALUE (可视为无界)。 |
| SynchronousQueue | 无容量队列,入队操作必须等待出队操作完成(同步传递元素)。 |
| PriorityBlockingQueue | 带优先级的无界阻塞队列,按优先级排序。 |
核心阻塞方法:
put(e):队列满时阻塞,直到有空间。take():队列为空时阻塞,直到有元素。
java
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(2);
// 入队(满时阻塞)
new Thread(() -> {
try {
blockingQueue.put(1);
blockingQueue.put(2);
blockingQueue.put(3); // 队列满,阻塞等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 出队(空时阻塞)
new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println(blockingQueue.take()); // 1,唤醒入队线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
三、队列应用
- 任务调度 :如线程池中的任务队列(
ThreadPoolExecutor使用BlockingQueue存储待执行任务)。 - 消息队列:分布式系统中,用队列缓冲消息(如 Kafka、RabbitMQ 的底层思想)。
- 广度优先搜索(BFS):遍历树或图时,用队列存储待访问节点。
- 缓冲处理:如 IO 流中的缓冲区,平衡数据读写速度差异。
- 单线程场景:优先用
ArrayDeque(高效)或LinkedList(灵活)。 - 优先级需求:用
PriorityQueue。 - 多线程并发:非阻塞用
ConcurrentLinkedQueue,阻塞用ArrayBlockingQueue或LinkedBlockingQueue。 - 固定容量:用
ArrayBlockingQueue或ArrayDeque(手动指定初始容量)。