想象一下排队买票的场景,先到的人先买票,后到的人排在队伍末尾,这就是队列的基本概念。
队列是一种遵循先进先出(FIFO, First-In-First-Out)原则的线性数据结构。
队列的主要特点包括:
两端操作:在一端(队尾)添加元素,在另一端(队头)移除元素,就像人们在队伍末尾加入,从队伍前端离开。
先进先出(FIFO):最先添加的元素最先被移除,就像排队买票,先到的人先服务。
访问受限:只能访问队头和队尾元素,无法直接访问队列中间的元素,就像你只能看到队伍的第一个人和最后一个人。
生活中的例子:
超市结账队伍:顾客按照到达顺序排队,先到的先结账。
打印任务队列:文档按照提交顺序排队等待打印。
餐厅等位:客人按照到达顺序获得座位。
交通信号灯:车辆按照到达顺序通过十字路口。
让我们通过生活中的例子来理解队列的基本操作:
操作:将元素添加到队尾。
时间复杂度:O(1),常数时间操作。
生活例子:新顾客加入排队队伍的末尾。
// 入队操作
public void enqueue(int item) {
if (isFull()) {
System.out.println("队列已满,无法添加元素");
return;
}
rear = (rear + 1) % capacity; // 循环队列实现
array[rear] = item;
size++;
System.out.println(item + " 已入队");
}
操作:移除并返回队头元素。
时间复杂度:O(1),常数时间操作。
生活例子:队伍最前面的人完成服务后离开。
// 出队操作
public int dequeue() {
if (isEmpty()) {
System.out.println("队列为空,无法移除元素");
return -1; // 表示错误
}
int item = array[front];
front = (front + 1) % capacity; // 循环队列实现
size--;
System.out.println(item + " 已出队");
return item;
}
操作:查看队头元素但不移除。
时间复杂度:O(1),常数时间操作。
生活例子:查看队伍最前面的人是谁,但不让他离开。
// 查看队头元素
public int peek() {
if (isEmpty()) {
System.out.println("队列为空,无法查看队头元素");
return -1; // 表示错误
}
return array[front]; // 只返回元素,不改变队列
}
操作:检查队列中是否有元素。
时间复杂度:O(1),常数时间操作。
生活例子:检查是否有人在排队。
// 检查队列是否为空
public boolean isEmpty() {
return size == 0; // 如果size为0,表示队列为空
}
队列可以通过两种主要方式实现:
特点:
生活例子:电影院有固定数量的座位,一旦满了就不能再容纳更多观众。
特点:
生活例子:根据需要可以无限延长的队伍,如网上购票系统的等待队列。
public class ArrayQueueExample {
static class ArrayQueue {
private int[] array; // 用于存储队列元素的数组
private int front; // 队头指针
private int rear; // 队尾指针
private int capacity; // 队列的容量
private int size; // 队列中元素的数量
// 构造函数,初始化队列
public ArrayQueue(int capacity) {
this.array = new int[capacity];
this.front = 0; // 初始化队头指针
this.rear = -1; // 初始化队尾指针
this.capacity = capacity;
this.size = 0;
}
// 添加元素到队尾
public void enqueue(int item) {
if (isFull()) {
System.out.println("队列已满,无法添加元素");
return;
}
rear = (rear + 1) % capacity; // 循环队列实现
array[rear] = item;
size++;
System.out.println(item + " 已入队");
}
// 移除并返回队头元素
public int dequeue() {
if (isEmpty()) {
System.out.println("队列为空,无法移除元素");
return -1; // 表示错误
}
int item = array[front];
front = (front + 1) % capacity; // 循环队列实现
size--;
System.out.println(item + " 已出队");
return item;
}
// 查看队头元素但不移除
public int peek() {
if (isEmpty()) {
System.out.println("队列为空,无法查看队头元素");
return -1; // 表示错误
}
return array[front];
}
// 检查队列是否为空
public boolean isEmpty() {
return size == 0;
}
// 检查队列是否已满
public boolean isFull() {
return size == capacity;
}
// 获取队列的大小
public int size() {
return size;
}
// 打印队列的内容
public void printQueue() {
if (isEmpty()) {
System.out.println("队列为空");
return;
}
System.out.print("队列内容(从队头到队尾): ");
int count = 0;
int index = front;
while (count < size) {
System.out.print(array[index]);
if (count < size - 1) System.out.print(" -> ");
index = (index + 1) % capacity;
count++;
}
System.out.println();
}
}
public static void main(String[] args) {
ArrayQueue queue = new ArrayQueue(5);
// 测试入队操作
queue.enqueue(10);
queue.enqueue(20);
queue.enqueue(30);
// 打印队列内容
queue.printQueue(); // 输出:队列内容(从队头到队尾): 10 -> 20 -> 30
// 测试查看队头元素
System.out.println("队头元素: " + queue.peek()); // 输出:队头元素: 10
// 测试出队操作
queue.dequeue(); // 输出:10 已出队
// 再次打印队列内容
queue.printQueue(); // 输出:队列内容(从队头到队尾): 20 -> 30
// 测试队列的大小
System.out.println("队列的大小: " + queue.size()); // 输出:队列的大小: 2
}
}
public class LinkedQueueExample {
static class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
static class LinkedQueue {
private Node front; // 队头指针
private Node rear; // 队尾指针
private int size; // 队列中元素的数量
public LinkedQueue() {
this.front = null;
this.rear = null;
this.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++;
System.out.println(item + " 已入队");
}
// 移除并返回队头元素
public int dequeue() {
if (isEmpty()) {
System.out.println("队列为空,无法移除元素");
return -1; // 表示错误
}
int item = front.data;
front = front.next;
// 如果队列变空,更新rear为null
if (front == null) {
rear = null;
}
size--;
System.out.println(item + " 已出队");
return item;
}
// 查看队头元素但不移除
public int peek() {
if (isEmpty()) {
System.out.println("队列为空,无法查看队头元素");
return -1; // 表示错误
}
return front.data;
}
// 检查队列是否为空
public boolean isEmpty() {
return front == null;
}
// 获取队列的大小
public int size() {
return size;
}
// 打印队列的内容
public void printQueue() {
if (isEmpty()) {
System.out.println("队列为空");
return;
}
System.out.print("队列内容(从队头到队尾): ");
Node current = front;
while (current != null) {
System.out.print(current.data);
if (current.next != null) System.out.print(" -> ");
current = current.next;
}
System.out.println();
}
}
public static void main(String[] args) {
LinkedQueue queue = new LinkedQueue();
// 测试入队操作
queue.enqueue(10);
queue.enqueue(20);
queue.enqueue(30);
// 打印队列内容
queue.printQueue(); // 输出:队列内容(从队头到队尾): 10 -> 20 -> 30
// 测试查看队头元素
System.out.println("队头元素: " + queue.peek()); // 输出:队头元素: 10
// 测试出队操作
queue.dequeue(); // 输出:10 已出队
// 再次打印队列内容
queue.printQueue(); // 输出:队列内容(从队头到队尾): 20 -> 30
// 测试队列的大小
System.out.println("队列的大小: " + queue.size()); // 输出:队列的大小: 2
}
}
队列有几种常见的变体,每种都有其特定的用途:
标准的FIFO队列,如上面实现的示例。
生活例子:超市结账队伍。
为了有效利用空间,将队列的头尾相连形成一个环。当队尾指针到达数组末尾时,它会绕回到数组开头。
生活例子:旋转餐厅的传送带,食物不断循环经过每个座位。
可以在两端(队头和队尾)进行插入和删除操作的队列。
生活例子:双向通行的走廊,人们可以从任一端进入或离开。
元素按优先级排序,而不是按照添加顺序。优先级最高的元素最先出队。
生活例子:医院急诊室的分诊系统,病情严重的患者优先就诊。
循环队列是队列的一种高效实现,它通过将队列的头尾相连,解决了普通队列在数组实现中可能出现的"假溢出"问题。
public class CircularQueueExample {
static class CircularQueue {
private int[] array; // 用于存储队列元素的数组
private int front; // 队头指针
private int rear; // 队尾指针
private int capacity; // 队列的容量
// 构造函数,初始化循环队列
public CircularQueue(int capacity) {
this.array = new int[capacity];
this.front = -1; // 初始化为-1,表示队列为空
this.rear = -1; // 初始化为-1,表示队列为空
this.capacity = capacity;
}
// 添加元素到队尾
public void enqueue(int item) {
if (isFull()) {
System.out.println("队列已满,无法添加元素");
return;
}
// 如果队列为空,初始化front
if (isEmpty()) {
front = 0;
}
// 循环移动rear指针
rear = (rear + 1) % capacity;
array[rear] = item;
System.out.println(item + " 已入队");
}
// 移除并返回队头元素
public int dequeue() {
if (isEmpty()) {
System.out.println("队列为空,无法移除元素");
return -1; // 表示错误
}
int item = array[front];
// 如果队列中只有一个元素
if (front == rear) {
front = -1;
rear = -1;
} else {
// 循环移动front指针
front = (front + 1) % capacity;
}
System.out.println(item + " 已出队");
return item;
}
// 查看队头元素但不移除
public int peek() {
if (isEmpty()) {
System.out.println("队列为空,无法查看队头元素");
return -1; // 表示错误
}
return array[front];
}
// 检查队列是否为空
public boolean isEmpty() {
return front == -1;
}
// 检查队列是否已满
public boolean isFull() {
return (front == 0 && rear == capacity - 1) || (rear == (front - 1) % (capacity - 1));
}
// 获取队列的大小
public int size() {
if (isEmpty()) {
return 0;
}
if (rear >= front) {
return rear - front + 1;
}
return capacity - front + rear + 1;
}
// 打印队列的内容
public void printQueue() {
if (isEmpty()) {
System.out.println("队列为空");
return;
}
System.out.print("队列内容(从队头到队尾): ");
int i = front;
do {
System.out.print(array[i]);
if (i != rear) System.out.print(" -> ");
i = (i + 1) % capacity;
} while (i != (rear + 1) % capacity);
System.out.println();
}
}
public static void main(String[] args) {
CircularQueue queue = new CircularQueue(5);
// 测试入队操作
queue.enqueue(10);
queue.enqueue(20);
queue.enqueue(30);
queue.enqueue(40);
// 打印队列内容
queue.printQueue(); // 输出:队列内容(从队头到队尾): 10 -> 20 -> 30 -> 40
// 测试出队操作
queue.dequeue(); // 输出:10 已出队
queue.dequeue(); // 输出:20 已出队
// 再次打印队列内容
queue.printQueue(); // 输出:队列内容(从队头到队尾): 30 -> 40
// 测试循环特性
queue.enqueue(50);
queue.enqueue(60);
queue.printQueue(); // 输出:队列内容(从队头到队尾): 30 -> 40 -> 50 -> 60
// 测试队列已满
queue.enqueue(70); // 输出:队列已满,无法添加元素
}
}
队列在计算机科学和日常生活中有许多应用:
任务调度
缓冲区管理
广度优先搜索(BFS)
消息队列
流媒体缓冲
等待系统
优点:
缺点:
特性 | 队列 | 栈 | 数组 |
---|---|---|---|
访问模式 | FIFO(先进先出) | LIFO(后进先出) | 随机访问 |
插入/删除位置 | 一端插入,另一端删除 | 同一端插入和删除 | 任意位置 |
随机访问 | 不支持 | 不支持 | 支持 |
主要应用 | 任务调度、缓冲区 | 函数调用、表达式求值 | 存储和访问数据 |
何时使用队列?
在本章中,我们学习了四种基本的数据结构:数组、链表、栈和队列。这些数据结构是构建更复杂数据结构和算法的基础。
数组:提供随机访问能力,但大小固定且插入删除操作可能需要移动元素。就像一排固定的座位,你可以直接找到任何一个座位,但要在中间加入新座位就需要移动其他座位。
链表:动态大小,高效的插入删除操作,但不支持随机访问。就像一条手拉手的人链,可以随时加入或离开,但要找到特定的人需要从头开始数。
栈:后进先出(LIFO)的数据结构,适用于需要逆序处理的场景。就像一堆盘子,只能从顶部添加或移除。
队列:先进先出(FIFO)的数据结构,适用于按顺序处理的场景。就像排队买票,先到的人先服务。
理解这些基本数据结构的特性和适用场景,对于选择合适的数据结构解决问题至关重要。每种数据结构都有其优缺点,适合不同的应用场景。在实际编程中,我们需要根据问题的特点选择最合适的数据结构,以提高程序的效率和可维护性。
《数据结构与算法分析》by Mark Allen Weiss - 深入讲解各种数据结构的实现和分析
《算法导论》第10章:基本数据结构 - 提供了数据结构的理论基础和数学分析
《Java编程思想》第16章:数组和集合 - 详细介绍了Java中的数组和集合框架
在线资源:
Java集合框架文档: