目录
开场白
在先前的几篇文章中,我们分别介绍了链表、栈等线性数据结构。除了这二者外,队列也是一种非常重要且经典的线性数据结构。
队列应用举例
现实生活
实际上,队列数据结构或队列思想在生活中比比皆是。我们在商场中购物结束,会来到收银台进行结账,在不同的收银台前都会排一个长长的队伍,尤其是在过年备年货的时候(比如近几天),那真的是人满为患。我们按照先来后到的顺序在收银台前排队,每当有一个顾客购物完毕需要来到收银台进行结账时,会来到待结账顾客队伍的队尾入队(排队);如果一个顾客完成结账,那就会从当前这个顾客队伍的队首出队,然后该干什么干什么。这其实就是经典的先来先服务(First Come First Server, FCFS)。
操作系统
在操作系统(Operating System,简称OS)中,队列也是一种非常重要的数据结构,广泛应用于各种操作系统场景中,我们简要介绍队列是如何在操作系统进程调度中扮演角色的。当某个进程(Process)完成创建态而转变成就绪态时,此时就意味着当前进程只要获取到了CPU执行权就可以上处理机运行,操作系统是如何维护这样的进程组呢?操作系统用一个名为就绪队列的队列维护这样的就绪进程,CPU按照一定的调度算法,在就绪队列中选择满足要求的进程让其上处理机运行,此时的进程状态就变为运行态,当进程要等待某个资源(如IO操作)而暂时无法继续执行,操作系统会将其转存到阻塞队列中,并将当前进程的状态修改为阻塞态,转而在就绪队列中去选择下一个满足要求的进程,让其上处理机运行。当阻塞队列中的阻塞态进程等待到了需要的资源,操作系统会让其重新进入就绪队列等待CPU调度。例如当操作系统使用时间片轮转(Round Robin, RR)调度算法进行进程调度时,每次会在就绪队列头部取出一个进程,为其分配一个指定大小的时间片,如果当前任务在时间片内完成,则将其从队列中彻底移除;如果当前任务在时间片结束时还未完成,则将其重新放回到就绪队列的队尾。
计算机网络
在计算机网络中,数据包的传输和处理是一个典型的队列应用场景。想象一下,你正在使用电脑浏览网页,每次点击链接或提交表单数据时,数据都会被打包成一个个小的数据包,通过网络传输到服务器。服务器接收到这些数据包后,需要对它们进行处理并返回响应。如果服务器同时收到太多的数据包,而处理能力有限,就会导致数据包积压。如果没有一个合理的机制来管理这些数据包,服务器可能会崩溃,或者某些用户的请求会被延迟甚至丢失。为了解决这个问题,服务器通常会使用队列来管理接收到的数据包。每当一个数据包到达服务器时,它会被放入队列的末尾;而服务器会从队列的头部依次取出数据包进行处理。这种先进先出的机制确保了数据包按照到达的顺序被处理。
从队列到循环队列
需要提前指出的是,循环队列与普通队列是两种不同的队列实现方案。前者是基于环形数组实现的,后者则是基于普通数组实现。但是二者的基本操作和应用场景实际上是相同的,只是前者的空间利用率和操作效率是更高的,我们本文通过介绍普通队列,阐述其在某些场景上的缺陷,进而引出循环队列。为了方便起见,后文中提到的所有普通队列我们都简称为队列。
队列的定义
队列(Queue)是一种先进先出(First In First Out, FIFO)的线性表,允许插入元素的一端称为队尾,允许删除的一端称为队首。队尾元素就是队列中的最后一个元素,队首元素就是队列中的第一个元素。例如我们给定一个元素序列 {
},即 {
},如果用队列来维护这个元素序列,那么元素
就是队首元素,元素
就是队尾元素。在队列的顺序存储结构中,为了方便起见,我们定义两个整型变量
和
分别表示队首指针与队尾指针,队首指针指向队首元素在队列中的存放下标,队尾指针指向队尾元素在队列中的存放下标
。假设我们在队列中依次添加元素 {
},示意图如下。
队列的不足
还是上面示意图中的队列,我们假设一个场景:继续向队列中添加元素 {
},示意图如下:
然后依次出队 个元素,此时队列就变成了如下图所示的结构。
此时队首指针指向当前队列中的队首元素 ,而队尾指针指向当前队列中队尾元素的下标
处,这个位置同时也是数组最后一个空间处。此时已经不能再入队了,如果再入队,
指针指向哪里呢?但是从示意图中我们不难发现,数组中还有
个空闲的可以存储元素的空间,这种现象我们称为假溢出。
循环队列的定义
不难发现,解决假溢出的方法之一就是从头再来。也就是让数组头尾相接构成环形,于是就可以引出循环队列的定义,我们把队列的这种头尾相接的顺序存储结构称为循环队列。需要指出的是,并不是在物理层面让数组头尾相接,而是逻辑上头尾相接,我们可以使用取模实现此需求。
于是继续我们上述的例子,在将元素 入队时,可以将元素存储在当前的
位置上,此时的
指针可以转变为指向下标
的位置,这样就不会造成指针指向不明的问题了,如下图。
此时,就又可以按照之前的逻辑继续执行入队操作,假设我们让元素序列 {
} 依次入队,此时的队列结构如下图所示。
此时队列中用于存放元素的数组还未被充分利用,可以再让一个元素入队,假设我们让元素 入队,此时队列结构如下图。此时的队首指针与队尾指针指向同一个位置,也就是
,意味着队列已满。
我们上面说明的所有情况都是已经存储了元素的队列,那么一个空队列如何表示呢?实际上当初始化一个队列的时候,仍然满足 ,这与队列判满的判断条件一致?我们不妨新定义一个变量
用来表示队列中存储元素的数目,那么此时的判空判满操作就可以通过比较
与数组长度
实现了。
实现循环队列
我们定义名为 的类用来实现循环队列。
1. 准备工作
为了为实现类提供统一规范,我们首先定义名为 的接口。
public interface Queue<E> {
boolean offer(E value);
E pull();
E peek();
boolean isEmpty();
boolean isFull();
}
让类 实现自定义接口
和可迭代接口
,队列中存储的元素数据类型是泛型,由用户自己决定存储的元素类型。
public class ArrayQueue<E> implements Queue<E>, Iterable<E>{}
根据前文的介绍,需要在类中提供几个成员变量,分别表示真正存储元素的数组、队首指针、队尾指针以及队列中存储的元素数量。
private E[] array;
private int head;
private int tail;
private int size;
2. 构造方法
为构造方法传入用来表示队列规模的参数 ,届时队列真正存储元素的数组就是根据此参数创建的,我加上了
镇压注解以忽略编译器提出的警告信息。
@SuppressWarnings("unchecked")
public ArrayQueue(int capacity) {
array = (E[]) new Object[capacity];
}
3. 判空 & 判满
在执行入队和出队操作之前,对队列满空状态做判断是很有必要的。由于在类中定义了表示队列存储元素数量的成员变量 ,所以判空与判满操作就可以基于对
的判断实现。
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean isFull() {
return size == array.length;
}
4. 入队
在执行入队操作之前,需要判断队列是否已满,如果已满,则无法将元素入队;前文中我们提到,队尾指针 用来表示当前队列队尾元素在数组中存储的下标
,换句话说,如果需要将一个新元素入队,新的元素就存放在数组中的
位置;在执行了入队操作后,需要更新队尾指针
,让其指向新队尾元素在数组中的存放下标
,但存在一种特殊情况,即此时的队尾指针已经指向了数组的最后一个存储位置
,此时需要让
指针回到数组的第一个位置,那么执行
%
,此时元素插入成功,更新表示队列中存储元素数量的
变量即可。
@Override
public boolean offer(E value) {
if (isFull())
return false;
array[tail] = value;
tail = (tail + 1) % array.length;
size++;
return true;
}
5. 出队
在执行出队操作之前,需要先判断队列此时是否为空,如果队列为空,说明此时队列中没有存放有效元素,返回 即可——这也是我们使用泛型的一个原因,不会因为返回值而产生歧义;如果队列此时不为空,返回队首指针指向的元素,并需要按照同样的取模规则,更新队首指针的指向。
@Override
public E pull() {
if (isEmpty())
return null;
E value = array[head];
head = (head + 1) % array.length;
size--;
return value;
}
6. 获取队首元素
同样的,需要先判断队列是否为空,如果队列为空,返回 即可;否则返回队首指针指向的元素即可,与出队操作不同的是,此时不需要再做额外的操作。
@Override
public E peek() {
if (isEmpty())
return null;
return array[head];
}
7. 遍历
与之前文章中对栈的遍历的分析类似,此时的遍历方法是一个伪遍历,按照真正的队列特性,只有将队首元素不断出队,不断更新队首指针才能完整得到队列中存储的所有元素,若按照这个思路,遍历一遍队列,队列中存储的元素也就瓦解了。
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int index = head;
@Override
public boolean hasNext() {
return index != tail;
}
@Override
public E next() {
E value = array[index];
index = (index + 1) % array.length;
return value;
}
};
}
final. 完整实现代码 & 单元测试
Java实现
/**
* @Author Arrebol
* @Date 2025/1/19 10:12
* @Project datastructure_algorithm
* @Description:
*/
public class ArrayQueue<E> implements Queue<E>, Iterable<E> {
private E[] array;
private int head;
private int tail;
private int size;
@SuppressWarnings("unchecked")
public ArrayQueue(int capacity) {
array = (E[]) new Object[capacity];
}
@Override
public boolean offer(E value) {
if (isFull())
return false;
array[tail] = value;
tail = (tail + 1) % array.length;
size++;
return true;
}
@Override
public E pull() {
if (isEmpty())
return null;
E value = array[head];
head = (head + 1) % array.length;
size--;
return value;
}
@Override
public E peek() {
if (isEmpty())
return null;
return array[head];
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean isFull() {
return size == array.length;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int index = head;
@Override
public boolean hasNext() {
return index != tail;
}
@Override
public E next() {
E value = array[index];
index = (index + 1) % array.length;
return value;
}
};
}
}
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Iterator;
class ArrayQueueTest {
private ArrayQueue<Integer> queue;
@BeforeEach
void setUp() {
queue = new ArrayQueue<>(5);
}
@Test
void testOfferAndPull() {
assertTrue(queue.offer(1));
assertTrue(queue.offer(2));
assertTrue(queue.offer(3));
assertEquals(1, queue.pull());
assertEquals(2, queue.pull());
assertEquals(3, queue.pull());
assertNull(queue.pull());
}
@Test
void testPeek() {
assertTrue(queue.offer(10));
assertTrue(queue.offer(20));
assertEquals(10, queue.peek());
assertEquals(10, queue.pull());
assertEquals(20, queue.peek());
}
@Test
void testIsEmpty() {
assertTrue(queue.isEmpty());
queue.offer(1);
assertFalse(queue.isEmpty());
queue.pull();
assertTrue(queue.isEmpty());
}
@Test
void testIsFull() {
assertFalse(queue.isFull());
for (int i = 0; i < 5; i++)
queue.offer(i);
assertTrue(queue.isFull());
assertFalse(queue.offer(6));
}
@Test
void testIterator() {
queue.offer(1);
queue.offer(2);
queue.offer(3);
Iterator<Integer> iterator = queue.iterator();
assertTrue(iterator.hasNext());
assertEquals(1, iterator.next());
assertEquals(2, iterator.next());
assertEquals(3, iterator.next());
assertFalse(iterator.hasNext());
queue.pull();
queue.pull();
queue.pull();
iterator = queue.iterator();
assertFalse(iterator.hasNext());
}
@Test
void testCircularBehavior() {
for (int i = 0; i < 5; i++) {
queue.offer(i);
}
assertEquals(0, queue.pull());
assertEquals(1, queue.pull());
assertTrue(queue.offer(5));
assertTrue(queue.offer(6));
assertEquals(2, queue.pull());
assertEquals(3, queue.pull());
assertEquals(4, queue.pull());
assertEquals(5, queue.pull());
assertEquals(6, queue.pull());
assertTrue(queue.isEmpty());
}
}
C语言实现
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct {
int *array;
int head;
int tail;
int size;
int capacity;
} ArrayQueue;
ArrayQueue* createQueue(int capacity) {
ArrayQueue* queue = (ArrayQueue*)malloc(sizeof(ArrayQueue));
queue->array = (int*)malloc(capacity * sizeof(int));
queue->head = 0;
queue->tail = 0;
queue->size = 0;
queue->capacity = capacity;
return queue;
}
bool offer(ArrayQueue* queue, int value) {
if (queue->size == queue->capacity)
return false;
queue->array[queue->tail] = value;
queue->tail = (queue->tail + 1) % queue->capacity;
queue->size++;
return true;
}
int pull(ArrayQueue* queue) {
if (queue->size == 0)
return -1; // Assuming -1 represents NULL in this context
int value = queue->array[queue->head];
queue->head = (queue->head + 1) % queue->capacity;
queue->size--;
return value;
}
int peek(ArrayQueue* queue) {
if (queue->size == 0)
return -1; // Assuming -1 represents NULL in this context
return queue->array[queue->head];
}
bool isEmpty(ArrayQueue* queue) {
return queue->size == 0;
}
bool isFull(ArrayQueue* queue) {
return queue->size == queue->capacity;
}
void freeQueue(ArrayQueue* queue) {
free(queue->array);
free(queue);
}
// Iterator implementation
typedef struct {
ArrayQueue* queue;
int index;
int count;
} QueueIterator;
QueueIterator createIterator(ArrayQueue* queue) {
QueueIterator iterator;
iterator.queue = queue;
iterator.index = queue->head;
iterator.count = 0;
return iterator;
}
bool hasNext(QueueIterator* iterator) {
return iterator->count < iterator->queue->size;
}
int next(QueueIterator* iterator) {
int value = iterator->queue->array[iterator->index];
iterator->index = (iterator->index + 1) % iterator->queue->capacity;
iterator->count++;
return value;
}
// Unit tests
void testOfferAndPull() {
ArrayQueue* queue = createQueue(5);
assert(offer(queue, 1));
assert(offer(queue, 2));
assert(offer(queue, 3));
assert(pull(queue) == 1);
assert(pull(queue) == 2);
assert(pull(queue) == 3);
assert(pull(queue) == -1); // Queue is empty
freeQueue(queue);
printf("testOfferAndPull passed\n");
}
void testPeek() {
ArrayQueue* queue = createQueue(5);
assert(offer(queue, 10));
assert(offer(queue, 20));
assert(peek(queue) == 10);
assert(pull(queue) == 10);
assert(peek(queue) == 20);
freeQueue(queue);
printf("testPeek passed\n");
}
void testIsEmpty() {
ArrayQueue* queue = createQueue(5);
assert(isEmpty(queue));
offer(queue, 1);
assert(!isEmpty(queue));
pull(queue);
assert(isEmpty(queue));
freeQueue(queue);
printf("testIsEmpty passed\n");
}
void testIsFull() {
ArrayQueue* queue = createQueue(5);
assert(!isFull(queue));
for (int i = 0; i < 5; i++)
assert(offer(queue, i));
assert(isFull(queue));
assert(!offer(queue, 6)); // Queue is full
freeQueue(queue);
printf("testIsFull passed\n");
}
void testIterator() {
ArrayQueue* queue = createQueue(5);
offer(queue, 1);
offer(queue, 2);
offer(queue, 3);
QueueIterator iterator = createIterator(queue);
assert(hasNext(&iterator));
assert(next(&iterator) == 1);
assert(next(&iterator) == 2);
assert(next(&iterator) == 3);
assert(!hasNext(&iterator));
pull(queue);
pull(queue);
pull(queue);
iterator = createIterator(queue);
assert(!hasNext(&iterator));
freeQueue(queue);
printf("testIterator passed\n");
}
void testCircularBehavior() {
ArrayQueue* queue = createQueue(5);
for (int i = 0; i < 5; i++) {
assert(offer(queue, i));
}
assert(pull(queue) == 0);
assert(pull(queue) == 1);
assert(offer(queue, 5));
assert(offer(queue, 6));
assert(pull(queue) == 2);
assert(pull(queue) == 3);
assert(pull(queue) == 4);
assert(pull(queue) == 5);
assert(pull(queue) == 6);
assert(isEmpty(queue));
freeQueue(queue);
printf("testCircularBehavior passed\n");
}
int main() {
testOfferAndPull();
testPeek();
testIsEmpty();
testIsFull();
testIterator();
testCircularBehavior();
printf("All tests passed!\n");
return 0;
}
实现链队列
链队列也是一种特殊的链表,需要指出的是,在队列背景下,元素的插入只能从队列尾部插入,元素的删除只能从队列头部删除,这里直接给出代码实现。
import java.util.Iterator;
/**
* @Author Arrebol
* @Date 2025/1/19 14:03
* @Project datastructure_algorithm
* @Description:
*/
public class LinkedListQueue<E> implements Queue<E>, Iterable<E> {
private static class QueueNode<E>{
E data;
QueueNode<E> next;
public QueueNode(E data, QueueNode<E> next) {
this.data = data;
this.next = next;
}
}
private QueueNode<E> head = new QueueNode<>(null, null);
private QueueNode<E> tail = head;
private final int capacity;
private int size = 0;
public LinkedListQueue(int capacity) {
this.capacity = capacity;
}
@Override
public boolean offer(E value) {
if (isFull())
return false;
tail.next = new QueueNode<>(value, null);
tail = tail.next;
size++;
return true;
}
@Override
public E pull() {
if (isEmpty())
return null;
E value = head.next.data;
head.next = head.next.next;
size--;
if (isEmpty())
tail = head;
return value;
}
@Override
public E peek() {
if (isEmpty())
return null;
return head.next.data;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean isFull() {
return size == capacity;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
QueueNode<E> indexNode = head.next;
@Override
public boolean hasNext() {
return indexNode != null;
}
@Override
public E next() {
E data = indexNode.data;;
indexNode = indexNode.next;
return data;
}
};
}
}
总结
本文先通过生活中的例子引出队列的概念,分析普通队列的缺陷进而引出循环队列,对循环队列常用操作进行简要分析,最后用代码实现。本文到此结束,后续将对单调队列和优先级队列进行简要分析与实现,敬请期待... ...