数据结构 —— 队列
队列是一种重要的线性数据结构,遵循 “先进先出”(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(这里牺牲一个位置区分空和满)
代码实现(循环队列):
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()); // 输出:1queue.enqueue(4);System.out.println(queue.peek()); // 输出:2System.out.println(queue.size()); // 输出:3}
}2. 基于链表的队列(链式队列)
- 原理:用链表存储元素,队头指向链表头节点,队尾指向链表尾节点。
- 优势:无需预先指定容量,动态扩容,避免溢出问题,实现简单。
- 劣势:相比数组,链表的指针操作会带来额外的内存开销。
代码实现(基于链表):
// 链表节点类
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()); // 输出:10System.out.println(queue.peek()); // 输出:20System.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 接口,有如下的队列核心操作:
操作类型 | 方法 | 说明 | 异常情况 |
入队 |
| 在队尾添加元素 | 队列满时抛出 |
入队 |
| 在队尾添加元素(推荐) | 队列满时返回 |
出队 |
| 移除并返回队头元素 | 队列为空时抛出 |
出队 |
| 移除并返回队头元素(推荐) | 队列为空时返回 |
查看队头 |
| 返回队头元素(不移除) | 队列为空时抛出 |
查看队头 |
| 返回队头元素(不移除,推荐) | 队列为空时返回 |
二、Queue 的主要实现类
1.LinkedList(链表队列)
- 底层结构:双向链表。
- 特点:
- 实现了
Queue和Deque接口,可作为队列或双端队列使用。 - 无容量限制(理论上受内存限制),不会出现队列满的情况。
- 入队 / 出队操作效率为
O(1),但因链表节点的内存开销,性能略低于数组实现。 - 适用场景:需要动态扩容、不确定元素数量的场景。
Queue<Integer> queue = new LinkedList<>();
queue.offer(1); // 入队
queue.offer(2);
System.out.println(queue.poll()); // 出队:1
System.out.println(queue.peek()); // 查看队头:22.ArrayDeque(双端队列)
- 底层结构:动态扩容的循环数组。
- 特点:
- 实现了
Deque接口,支持两端入队 / 出队(兼具队列和栈的功能)。 - 初始容量为 8,当元素满时自动扩容(翻倍)。
- 入队 / 出队操作效率为
O(1),数组结构减少了链表的指针开销,性能优于LinkedList。 - 适用场景:需要高效双端操作、频繁入队出队的场景(推荐作为队列 / 栈的首选)。
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(10);
queue.offer(20);
System.out.println(queue.poll()); // 103.PriorityQueue(优先级队列)
- 底层结构:基于二叉堆(小顶堆)。
- 特点:
- 不遵循 FIFO,而是按元素优先级排序(默认自然顺序,可自定义
Comparator)。 - 队头始终是优先级最高的元素(如最小元素)。
- 入队操作
O(log n),出队操作O(log n)。 - 适用场景:需要按优先级处理任务的场景(如调度系统)
// 默认小顶堆(元素从小到大出队)
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 的子接口,增加了阻塞操作(当队列满 / 空时,线程会阻塞等待),专为多线程协作设计。
主要实现类有:
实现类 | 特点 |
| 基于数组的有界阻塞队列,容量固定,支持公平 / 非公平锁。 |
| 基于链表的阻塞队列,默认容量为 (可视为无界)。 |
| 无容量队列,入队操作必须等待出队操作完成(同步传递元素)。 |
| 带优先级的无界阻塞队列,按优先级排序。 |
核心阻塞方法:
put(e):队列满时阻塞,直到有空间。take():队列为空时阻塞,直到有元素。
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(手动指定初始容量)。
