队列简介
队列是一种线性数据结构,遵循“先进先出”(FIFO)原则,即第一个进入队列的元素将第一个被移除。想象一个排队的队伍,排在最前面的人会先被服务。队列广泛应用于打印任务管理、网页服务器请求处理或模拟现实场景,如排队系统。
本文将探讨两种常见的队列实现方式:基于数组的循环队列和基于链表的队列。我们将提供代码示例,详细解释其工作原理,并进行对比分析。
队列操作
在深入实现之前,先定义队列的关键操作:
-
入队(Enqueue):在队列尾部添加一个元素。
-
出队(Dequeue):从队列头部移除一个元素。
-
查看队头(Front):查看队列头部的元素但不移除。
-
是否为空(IsEmpty):检查队列是否为空。
-
是否已满(IsFull):检查队列是否已达到最大容量(对于固定大小的队列)。
基于数组的队列:循环队列
当使用数组实现队列时,面临一个挑战:数组大小固定,一旦队尾到达数组末尾,即使开头有空位(因出队而空出),也无法添加更多元素。为解决此问题,我们采用循环队列,将数组视为环形,队尾到达末尾时可绕回开头。
结构
我们定义一个循环队列结构,包括:
-
一个数组存储元素。
-
两个整数
front和rear,分别跟踪队头和队尾位置。 -
数组大小。
在循环队列中,通常留一个空位以区分满和空状态。
初始化
-
将
front和rear设置为 0。
是否为空
-
当
front == rear时,队列为空。
是否已满
-
当
(rear + 1) % MAXSIZE == front时,队列已满。
入队
-
如果队列已满,返回失败。
-
否则,将元素放入
data[rear],然后rear = (rear + 1) % MAXSIZE。
出队
-
如果队列为空,返回失败。
-
否则,获取
element = data[front],然后front = (front + 1) % MAXSIZE,返回element。
查看队头
-
如果队列为空,返回失败。
-
否则,返回
data[front]。
基于数组的队列实现(循环队列)
基于数组的队列使用固定大小的数组存储元素,为了高效利用空间,通常采用循环队列的方式实现。循环队列通过模运算(%)实现数组的“循环”,当队列尾部到达数组末尾时,可以从数组开头继续添加元素。
代码示例
以下是基于数组的循环队列实现
#define MAXSIZE 1024 // 队列的最大容量
typedef struct {
int data[MAXSIZE]; // 存储元素的数组,最大容量为 MAXSIZE - 1
int front; // 队头索引
int rear; // 队尾索引
} ArrayQueue;
// 初始化队列
void initArrayQueue(ArrayQueue* q) {
q->front = 0; // 队头索引初始化为 0
q->rear = 0; // 队尾索引初始化为 0
}
// 检查队列是否为空
int isEmpty(ArrayQueue* q) {
return q->front == q->rear; // 当 front == rear 时,队列为空
}
// 检查队列是否已满
int isFull(ArrayQueue* q) {
return (q->rear + 1) % MAXSIZE == q->front; // 当 (rear + 1) % MAXSIZE == front 时,队列已满
}
// 入队
int enqueue(ArrayQueue* q, int element) {
if (isFull(q)) {
return 0; // 队列已满,返回 0 表示失败
}
q->data[q->rear] = element; // 在队尾添加元素
q->rear = (q->rear + 1) % MAXSIZE; // 更新队尾索引,循环
return 1; // 成功
}
// 出队
int dequeue(ArrayQueue* q) {
if (isEmpty(q)) {
return -1; // 队列为空,返回 -1 表示失败
}
int element = q->data[q->front]; // 获取队头元素
q->front = (q->front + 1) % MAXSIZE; // 更新队头索引,循环
return element; // 返回出队元素
}
// 查看队头元素
int front(ArrayQueue* q) {
if (isEmpty(q)) {
return -1; // 队列为空,返回 -1
}
return q->data[q->front]; // 返回队头元素
}
详细解释
-
为什么使用循环队列?
循环队列允许队列在数组中“循环”,当队尾(rear)到达数组末尾时,可以从数组开头继续添加元素,从而提高空间利用率。例如,如果数组大小为 5,当rear = 4时,(rear + 1) % 5 = 0,即下一个位置是 0。 -
front 和 rear 的含义:
-
front指向队头元素(即将被移除的元素)。 -
rear指向队尾的下一个位置(因为入队时,我们先放元素再移动rear)。
-
-
isEmpty 和 isFull 的判断:
-
当
front == rear时,队列为空。 -
当
(rear + 1) % MAXSIZE == front时,队列已满(留一个空间区分空和满)。这是为了避免空队列和满队列的歧义。
-
-
% MAXSIZE 的作用:
实现循环,使用模运算使索引在 0 到MAXSIZE-1之间循环。例如,如果MAXSIZE = 5,当rear = 4时,(rear + 1) % 5 = 0,即下一个位置是 0。 -
注意:
由于留一个空间区分空和满,队列的最大容量为MAXSIZE - 1。例如,如果MAXSIZE = 1024,队列最多能存储 1023 个元素。 -
为什么 return -1 表示失败?
在dequeue和front中,返回 -1 表示队列为空,无法操作。这是 C 语言中常见的约定,因为 -1 不是有效索引,适合表示“失败”。
从 Circular Queue - GeeksforGeeks 中提到,循环队列通过模运算实现循环,留一个空间区分空满,这与我们的实现一致。
性能
-
入队和出队时间复杂度:O(1),因为涉及简单数组访问和索引更新。
-
空间复杂度:O(n),其中 n 为
MAXSIZE,在编译时固定。
优点
-
实现简单,初学者易懂。
-
由于连续内存,访问速度快,缓存友好。
-
适合最大大小已知的场景。
缺点
-
固定大小限制增长;一旦达到
MAXSIZE - 1,无法添加更多元素(本实现未处理动态调整)。 -
如果队列未满,可能浪费空间,导致内存效率低。
基于链表的队列实现
基于链表的队列使用节点链表存储元素,维护两个指针:front(队头)和 rear(队尾),以实现高效的入队和出队操作。链表队列动态调整大小,仅受可用内存限制。
代码示例
以下是基于链表的队列实现:
#include <stdio.h>
#include <stdlib.h>
// 定义节点结构
typedef struct Node {
int data; // 存储数据
struct Node* next; // 指向下一个节点的指针
} Node;
// 定义队列结构
typedef struct {
Node* front; // 队头指针
Node* rear; // 队尾指针
} LinkedListQueue;
// 初始化队列
void initLinkedListQueue(LinkedListQueue* q) {
q->front = NULL; // 队头指针初始为 NULL
q->rear = NULL; // 队尾指针初始为 NULL
}
// 入队
int enqueue(LinkedListQueue* q, int element) {
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
return 0; // 内存分配失败,返回 0
}
newNode->data = element; // 设置新节点的数据
newNode->next = NULL; // 新节点的 next 为 NULL
if (q->rear == NULL) { // 队列为空
q->front = newNode; // 队头指向新节点
q->rear = newNode; // 队尾指向新节点
} else {
q->rear->next = newNode; // 将新节点链接到队尾
q->rear = newNode; // 更新队尾指针
}
return 1; // 成功
}
// 出队
int dequeue(LinkedListQueue* q) {
if (q->front == NULL) {
return -1; // 队列为空,返回 -1
}
Node* temp = q->front; // 保存队头节点
int element = temp->data; // 获取队头数据
q->front = q->front->next; // 更新队头指针
if (q->front == NULL) { // 队列变为空
q->rear = NULL;
}
free(temp); // 释放内存
return element; // 返回出队元素
}
// 查看队头元素
int front(LinkedListQueue* q) {
if (q->front == NULL) {
return -1; // 队列为空,返回 -1
}
return q->front->data; // 返回队头数据
}
// 检查队列是否为空
int isEmpty(LinkedListQueue* q) {
return q->front == NULL; // 如果队头为 NULL,队列为空
}
详细解释
-
front 和 rear 的含义:
-
front指向队头节点(即将被移除的元素)。 -
rear指向队尾节点(即将添加新元素的位置)。
-
-
NULL 的作用:
-
当队列为空时,
front和rear都为NULL,表示没有节点。 -
当一个节点的
next为NULL时,表示这是最后一个节点,链表结束。
-
-
两个 -> 的用法:
-
例如在入队时,
q->rear->next = newNode表示通过rear指针访问队尾节点的next成员,并将其指向新节点。 -
在出队时,
q->front->next表示通过front指针访问队头节点的next成员,获取下一个节点的指针。
-
-
动态大小:
链表队列没有固定大小限制,仅受可用内存限制。例如,在入队时通过malloc动态分配新节点。 -
为什么 return -1 表示失败?
在dequeue和front中,返回 -1 表示队列为空,无法操作。这是 C 语言中常见的约定,因为 -1 不是有效索引,适合表示“失败”。
从 Implement Queue using Linked List - GeeksforGeeks 中提到,链表队列通过 front 和 rear 实现 O(1) 的入队和出队,这与我们的实现一致。
性能
-
入队和出队时间复杂度:O(1),因为涉及指针操作和动态内存分配。
-
空间复杂度:O(n),其中 n 为元素数,内存动态分配。
优点
-
动态大小:队列可按需增长,仅受可用内存限制,适合未知或可变大小。
-
无浪费空间:只为当前元素分配内存,提高内存效率。
缺点
-
由于动态内存分配和指针解引用,比数组稍慢,可能缓存效率低。
-
需要手动管理内存(如释放节点),若处理不当可能导致内存泄漏。
9468

被折叠的 条评论
为什么被折叠?



