目录
1 队列的基本概念
1.1 定义
队列(Queue)是一种特殊类型的线性表,其特点是只允许在一端进行插入(入队或进队)操作,在另一端进行删除(出队或离队)操作。这种数据结构遵循先进先出(First In First Out, FIFO)的原则,即最先加入队列的元素将最先被移除。
1.2 队列的组成部分
队首(front): 队列中允许进行删除操作的一端。当元素从队列中移除时,总是从这一端开始。
队尾(rear): 队列中允许进行插入操作的一端。新元素总是添加到这一端。
1.3 空队列
当队列中没有任何元素时,我们称其为空队列。此时,队首和队尾的概念暂时不存在,直到有新的元素加入队列。
1.4 操作流程
假设有一个空队列,当我们依次向其中添加元素 a1, a2, ..., an 后,a1 将成为队首元素,而 an 则成为队尾元素。因此,当需要从队列中移除元素时,按照 FIFO 原则,移除的顺序也必然是 a1, a2, ..., an。类似于生活中的排队行为。
1.4.1 添加元素(入队)
在添加元素时,通常的步骤如下:
- 检查队列是否已满:在添加元素之前,首先需要检查队列是否已满。如果队列已满,则不能继续添加元素。
- 存放元素:将新元素存放到队列的队尾位置。
- 移动队尾指针:将队尾指针
rear
向前移动一位,指向下一个空闲位置。
1.4.2 删除元素(出队)
在删除元素时,通常的步骤如下:
- 检查队列是否为空:在删除元素之前,首先需要检查队列是否为空。如果队列为空,则不能继续删除元素。
- 获取队首元素:将队首元素保存在一个临时变量中,以便返回。
- 移动队首指针:将队首指针
front
向前移动一位,指向下一个元素。 - 返回队首元素:返回保存的队首元素。
2 队列的存储结构
队列可以采用不同的方式来实现,主要分为以下两种:
2.1 顺序队列
顺序队列使用数组作为底层存储结构。在这种情况下,队列的元素连续地存储在数组中。为了高效地管理队列,通常会维护两个指针,一个指向队首,另一个指向队尾。然而,顺序队列存在一个潜在的问题,即 “假溢出” 现象,即队列的实际存储空间未满,但由于队首和队尾指针的位置关系导致无法继续插入新的元素。
2.2 链式队列
链式队列利用链表作为底层存储结构。每个节点包含数据部分和指向下一个节点的指针。与顺序队列相比,链式队列没有固定的容量限制,可以根据需要动态增加或减少内存。链式队列的优点在于它能够避免顺序队列中的 “假溢出” 问题,并且更容易实现队列的动态扩展。然而,链式队列的访问效率可能不如顺序队列,因为链表中的元素不是连续存储的。
3 顺序队列中的 “假溢出” 问题及解决方案
3.1 问题描述
在顺序队列中,如果队列的尾指针 rear 已经到达数组的末尾,即使队列中有许多空闲位置(前面的元素删除后就会有位置),也无法再进行入队操作。这种情况被称为 “假溢出”。如下图所示:
3.2 解决方案
方法1:元素左移法
入队操作:
- 将新元素添加到队列的最后一个位置。
出队操作:
- 移除队首元素。
- 将队列中剩余的所有元素向左移动一位,以填补空缺。
优点:
- 实现简单,不需要额外的空间。
缺点:
- 每次出队操作都需要移动所有后续元素,时间复杂度为 O(n),效率较低。
方法2:循环队列
概念:
将队列的存储空间视为一个首尾相接的圆环,即当尾指针 rear 到达数组末尾时,可以将其重置为数组的起始位置,一般通过模运算进行操作。
实现:
- 使用两个指针 front 和 rear 分别指向队首和队尾。
- 当 rear 达到数组末尾时,将其设置为数组的起始位置(queue->rear = (queue->rear + 1) % queue->capacity)。例如,假设队列的容量 capacity 为 5,rear 为 4(即数组的最后一个位置),那么 (4 + 1) % 5 的结果是 0,rear 会被更新为 0,即数组的起始位置。
- 同样,当 front 达到数组末尾时,将其设置为数组的起始位置(queue->front = (queue->front + 1) % queue->capacity)。例如,假设队列的容量 capacity 为 5,front 为 4(即数组的最后一个位置),那么 (4 + 1) % 5 的结果是 0,front 会被更新为 0,即数组的起始位置。
优点:
- 高效利用存储空间,避免了 “假溢出” 问题。
- 入队和出队操作的时间复杂度均为 O(1)。
缺点:
需要额外处理队列满和队列空的情况。
4 功能定义
前文已提及,队列的底层实现既可以选择数组也可以选择链表。本节将基于动态数组来实现一个顺序队列,并定义以下必需的功能函数:
功能 | 函数签名 | 描述 |
---|---|---|
初始化队列 | void initQueue(Queue *queue, size_t capacity) | 初始化一个队列,指定其容量。 |
返回队列内元素个数 | size_t getSize(const Queue *queue) | 返回队列中当前的元素个数。 |
添加新元素 | void enqueue(Queue *queue, int element) | 将一个新元素(假设是 int 类型)添加到队列的队尾。 |
元素出队列 | int dequeue(Queue *queue) | 从队列的队首移除一个元素并返回该元素(假设是 int 类型)。 |
释放队列内存 | void destroyQueue(Queue *queue) | 释放队列占用的内存。 |
遍历队列 | void printQueue(Queue *queue) | 打印队列中的所有元素。 |
5 功能实现
本节将实现一个队列,基于动态数组(即顺序队列)以提供灵活性,并引入循环队列以解决假溢出问题。
5.1 初始化队列
#include <stdio.h>
#include <stdlib.h>
// 队列结构
typedef struct
{
int *data; // 动态数组存储队列元素
size_t size; // 队列内元素个数
size_t capacity; // 动态数组的容量
size_t front; // 队列头指针
size_t rear; // 队列尾指针
} Queue;
/**
* 初始化队列
*
* 此函数用于初始化一个队列,并分配初始容量的内存。
*
* @param queue 指向队列结构体的指针
* @param initialCapacity 队列的初始容量
*
* @return 无返回值
*
* @note
* - 如果传入的 `queue` 指针为 NULL,函数将输出错误信息并返回。
* - 如果传入的 `initialCapacity` 为 0,函数将输出警告信息并返回。
* - `size_t` 是 C 标准库中定义的一种无符号整数类型,不能表示负数。因此,传入的 `initialCapacity` 应该是一个正数。
* - 成功初始化队列后,函数将输出一条成功消息。
*/
void initQueue(Queue *queue, size_t initialCapacity)
{
// 检查传入的指针是否为空
if (queue == NULL)
{
puts("Error: 传入的队列为 NULL");
return; // 如果是空指针,则直接返回,退出函数
}
// size_t 是 C 标准库中定义的一种无符号整数类型,不能表示负数
// 如果传入的 initialCapacity 是负数,可能会表示为一个很大的数(补码、数据回绕处理)
// 函数内部不方便处理负数行为,只能在函数调用之前,约束传入的参数
// 所以这里只是检查是否为 0
if (initialCapacity == 0)
{
puts("Warning: 初始容量为 0,请使用一个正数来表示容量!");
return; // 如果容量为 0,退出函数
}
// 分配初始容量的内存
queue->data = (int *)malloc(initialCapacity * sizeof(int));
if (queue->data == NULL)
{
puts("Error: 内存分配失败,队列初始化失败");
return; // 如果内存分配失败,退出函数
}
queue->size = 0; // 初始元素个数为 0
queue->capacity = initialCapacity; // 设置容量
queue->front = 0; // 队列头指针
queue->rear = 0; // 队列尾指针
printf("Success: 成功初始化动态数组,容量为:%zu\n", initialCapacity);
}
int main()
{
Queue myQueue;
puts("----------------------1.初始化队列----------------------");
initQueue(&myQueue, 2);
initQueue(NULL, 2); // 错误测试
initQueue(&myQueue, 0); // 错误测试
return 0;
}
输出结果如下所示:
5.2 返回队列内元素个数
/**
* 返回队列内元素个数
*
* 此函数用于获取队列中当前的元素个数。
*
* @param queue 指向队列结构体的指针
*
* @return 队列中的元素个数
*
* @note
* - 如果传入的 `queue` 指针为 NULL,函数将输出错误信息并返回 0。
* - 该函数不会修改队列的状态。
*/
size_t getSize(const Queue *queue)
{
// 检查传入的指针是否为空
if (queue == NULL)
{
puts("Error: 传入的队列为 NULL");
return 0; // 如果是空指针,则直接返回 0,退出函数
}
return queue->size;
}
5.3 添加新元素(入队)
/**
* 添加新元素
*
* 此函数用于将一个新元素添加到队列的尾部。
*
* @param queue 指向队列结构体的指针
* @param element 要添加的新元素
*
* @return 无返回值
*
* @note
* - 如果传入的 `queue` 指针为 NULL,函数将输出错误信息并返回。
* - 如果队列已满,函数将输出错误信息并返回。
* - 该函数会更新队列的尾指针和元素个数。
*/
void enqueue(Queue *queue, int element)
{
// 检查传入的指针是否为空
if (queue == NULL)
{
puts("Error: 传入的队列为 NULL");
return;
}
// 检查队列是否已满
if (queue->size == queue->capacity)
{
printf("队列已满,元素添加失败\n");
// 暂时先不进行循环队列的扩容,因为循环队列的扩容有点麻烦
return;
}
// 将新元素添加到队列尾部
// 添加元素,只能在队尾进行
queue->data[queue->rear] = element;
// 循环更新队列尾指针
// queue->rear++; // 这种简单的方式,会出现【假溢出】问题
// 应该使用下面这种方式
queue->rear = (queue->rear + 1) % queue->capacity;
// 例如,假设 capacity 为 5,rear 为 4(即数组的最后一个位置)
// 那么 (4 + 1) % 5 的结果是 0,rear 会被更新为 0,即数组的起始位置。
// 更新元素个数
queue->size++;
printf("Success:元素(%d)已成功添加到队列\n", element);
}
5.4 循环队列的扩容问题
循环队列的扩容比普通队列的扩容要复杂一些。主要原因在于循环队列的元素可能分布在数组的不同部分,而且扩容时需要保证元素的相对顺序不变。以下是详细的解释:
操作复杂性
-
元素分布不连续
- 在循环队列中,元素可能分布在数组的不同部分。例如,假设队列的容量为 5,
front
为 3,rear
为 1,size
为 4,数组的状态可能是[4, _, 1, 2, 3]
。在这种情况下,元素 1、2、3、4 分布在数组的不同部分。
- 在循环队列中,元素可能分布在数组的不同部分。例如,假设队列的容量为 5,
-
保持元素顺序
- 扩容时,需要将现有的元素按顺序复制到新的更大的数组中。由于元素在原数组中分布不连续,直接复制会导致顺序混乱。
-
重新计算指针
- 扩容后,需要重新计算
front
和rear
指针的位置,以确保它们在新的数组中仍然指向正确的元素。
- 扩容后,需要重新计算
具体步骤
-
创建新数组
- 创建一个新的数组,容量通常是原数组的两倍或更大。
-
复制元素
- 从
front
开始,逐个将元素复制到新数组中,直到复制完size
个元素。这一步需要使用模运算来处理循环特性。
- 从
-
更新指针
- 更新
front
和rear
指针,使其在新数组中指向正确的元素。通常,新的front
会设为 0,新的rear
会设为size
。
- 更新
-
释放旧数组
- 释放原来的数组,将新数组赋值给队列的
data
指针。
- 释放原来的数组,将新数组赋值给队列的
提示:
由于循环队列的扩容涉及复杂的元素复制和指针更新,这里不进行单独的代码展示。
5.5 删除元素(出队)并返回
/**
* 元素出队列
*
* 此函数用于从队列的头部移除一个元素。
*
* @param queue 指向队列结构体的指针
*
* @return 移除的元素,如果队列为空则返回 -1
*
* @note
* - 如果传入的 `queue` 指针为 NULL,函数将输出错误信息并返回 -1。
* - 如果队列为空,函数将返回 -1。
* - 该函数会更新队列的头指针和元素个数。
*/
int dequeue(Queue *queue)
{
if (queue == NULL)
{
puts("Error: 传入的队列为 NULL");
return -1;
}
// 注意:不能通过判断 front 和 rear 是否相等来判定队列是否为空
// front 和 rear 相等,可以是队列空了也可以是队列满了
// 需要通过 queue->size 来判断
if (queue->size == 0)
{
puts("Warning: 队列中已无元素可删除");
return -1; // 队列为空,返回无效值
}
// 获取要出队的元素
// 删除元素,只能在队首进行
int dequeuedElement = queue->data[queue->front];
// 循环更新队列头指针
queue->front = (queue->front + 1) % queue->capacity;
// 更新元素个数
queue->size--;
return dequeuedElement; // 返回出队的元素
}
5.6 释放队列内存
/**
* 释放队列内存
*
* 此函数用于释放队列所占用的内存,并重置队列的各个属性。
*
* @param queue 指向队列结构体的指针
*
* @return 无返回值
*
* @note
* - 如果传入的 `queue` 指针为 NULL,函数将输出错误信息并返回。
* - 该函数会释放动态数组内存,并将队列的各个属性重置为初始状态。
*/
void destroyQueue(Queue *queue)
{
if (queue == NULL)
{
puts("Error: 传入的队列为 NULL");
return;
}
free(queue->data); // 释放动态数组内存
queue->data = NULL; // 避免悬挂指针
queue->size = 0;
queue->capacity = 0;
queue->front = 0;
queue->rear = 0;
puts("Success:队列所占用的内存已成功释放^_^");
}
5.7 遍历队列
注意,循环队列的设计使得队列的头指针 front 和尾指针 rear 可能在数组中不是连续的。具体来说,当 rear 达到数组末尾时,它会循环回到数组的起始位置。因此,队列中的元素可能分布在数组的不同部分。所以在遍历循环队列时,不能通过简单的循环来进行遍历,具体操作应如下所示:
/**
* 遍历队列
*
* 此函数用于遍历队列中的所有元素并打印。
*
* @param queue 指向队列结构体的指针
*
* @return 无返回值
*
* @note
* - 如果传入的 `queue` 指针为 NULL,函数将输出错误信息并返回。
* - 该函数会从队列头指针开始,遍历队列中的所有元素并打印。
*/
void printQueue(Queue *queue)
{
if (queue == NULL)
{
puts("Error: 传入的队列为 NULL");
return; // 队列为空指针,退出函数
}
if (queue->size == 0)
{
puts("Warning: 队列为空队列,已无元素可遍历");
return; // 队列为空,退出函数
}
puts("开始遍历队列元素:");
// 不能使用下面这种行为进行遍历
// 循环队列的设计使得队列的头指针 front 和尾指针 rear 可能在数组中不是连续的。
// 具体来说,当 rear 达到数组末尾时,它会循环回到数组的起始位置。
// 因此,队列中的元素可能分布在数组的不同部分。
// for (int i = 0; i < queue->size; i++)
// {
// printf("%d", queue->data[i]);
// }
// 正确的做法是从 front 开始遍历,直到遍历完 size 个元素
for (int i = queue->front, j = 0; j < queue->size; i++, j++)
{
int data = queue->data[i % queue->capacity];
// i % queue->capacity 确保 i 始终在数组的有效索引范围内。
// 例如,假设 capacity 为 5,i 为 4,那么(4 + 1) % 5 的结果是 0,
// 这样 i 会从 4 变为 0,即数组的起始位置。 printf("%d ", data);
printf("%d ", data);
}
printf("\n");
}
5.8 完整代码
#include <stdio.h>
#include <stdlib.h>
// 队列结构
typedef struct
{
int *data; // 动态数组存储队列元素
size_t size; // 队列内元素个数
size_t capacity; // 动态数组的容量
size_t front; // 队列头指针
size_t rear; // 队列尾指针
} Queue;
/**
* 初始化队列
*
* 此函数用于初始化一个队列,并分配初始容量的内存。
*
* @param queue 指向队列结构体的指针
* @param initialCapacity 队列的初始容量
*
* @return 无返回值
*
* @note
* - 如果传入的 `queue` 指针为 NULL,函数将输出错误信息并返回。
* - 如果传入的 `initialCapacity` 为 0,函数将输出警告信息并返回。
* - `size_t` 是 C 标准库中定义的一种无符号整数类型,不能表示负数。因此,传入的 `initialCapacity` 应该是一个正数。
* - 成功初始化队列后,函数将输出一条成功消息。
*/
void initQueue(Queue *queue, size_t initialCapacity)
{
// 检查传入的指针是否为空
if (queue == NULL)
{
puts("Error: 传入的队列为 NULL");
return; // 如果是空指针,则直接返回,退出函数
}
// size_t 是 C 标准库中定义的一种无符号整数类型,不能表示负数
// 如果传入的 initialCapacity 是负数,可能会表示为一个很大的数(补码、数据回绕处理)
// 函数内部不方便处理负数行为,只能在函数调用之前,约束传入的参数
// 所以这里只是检查是否为 0
if (initialCapacity == 0)
{
puts("Warning: 初始容量为 0,请使用一个正数来表示容量!");
return; // 如果容量为 0,退出函数
}
// 分配初始容量的内存
queue->data = (int *)malloc(initialCapacity * sizeof(int));
if (queue->data == NULL)
{
puts("Error: 内存分配失败,队列初始化失败");
return; // 如果内存分配失败,退出函数
}
queue->size = 0; // 初始元素个数为 0
queue->capacity = initialCapacity; // 设置容量
queue->front = 0; // 队列头指针
queue->rear = 0; // 队列尾指针
printf("Success: 成功初始化动态数组,容量为:%zu\n", initialCapacity);
}
/**
* 返回队列内元素个数
*
* 此函数用于获取队列中当前的元素个数。
*
* @param queue 指向队列结构体的指针
*
* @return 队列中的元素个数
*
* @note
* - 如果传入的 `queue` 指针为 NULL,函数将输出错误信息并返回 0。
* - 该函数不会修改队列的状态。
*/
size_t getSize(const Queue *queue)
{
// 检查传入的指针是否为空
if (queue == NULL)
{
puts("Error: 传入的队列为 NULL");
return 0; // 如果是空指针,则直接返回 0,退出函数
}
return queue->size;
}
/**
* 添加新元素
*
* 此函数用于将一个新元素添加到队列的尾部。
*
* @param queue 指向队列结构体的指针
* @param element 要添加的新元素
*
* @return 无返回值
*
* @note
* - 如果传入的 `queue` 指针为 NULL,函数将输出错误信息并返回。
* - 如果队列已满,函数将输出错误信息并返回。
* - 该函数会更新队列的尾指针和元素个数。
*/
void enqueue(Queue *queue, int element)
{
// 检查传入的指针是否为空
if (queue == NULL)
{
puts("Error: 传入的队列为 NULL");
return;
}
// 检查队列是否已满
if (queue->size == queue->capacity)
{
printf("队列已满,元素添加失败\n");
// 暂时先不进行循环队列的扩容,因为循环队列的扩容有点麻烦
return;
}
// 将新元素添加到队列尾部
// 添加元素,只能在队尾进行
queue->data[queue->rear] = element;
// 循环更新队列尾指针
// queue->rear++; // 这种简单的方式,会出现【假溢出】问题
// 应该使用下面这种方式
queue->rear = (queue->rear + 1) % queue->capacity;
// 例如,假设 capacity 为 5,rear 为 4(即数组的最后一个位置)
// 那么 (4 + 1) % 5 的结果是 0,rear 会被更新为 0,即数组的起始位置。
// 更新元素个数
queue->size++;
printf("Success:元素(%d)已成功添加到队列\n", element);
}
/**
* 元素出队列
*
* 此函数用于从队列的头部移除一个元素。
*
* @param queue 指向队列结构体的指针
*
* @return 移除的元素,如果队列为空则返回 -1
*
* @note
* - 如果传入的 `queue` 指针为 NULL,函数将输出错误信息并返回 -1。
* - 如果队列为空,函数将返回 -1。
* - 该函数会更新队列的头指针和元素个数。
*/
int dequeue(Queue *queue)
{
if (queue == NULL)
{
puts("Error: 传入的队列为 NULL");
return -1;
}
// 注意:不能通过判断 front 和 rear 是否相等来判定队列是否为空
// front 和 rear 相等,可以是队列空了也可以是队列满了
// 需要通过 queue->size 来判断
if (queue->size == 0)
{
puts("Warning: 队列中已无元素可删除");
return -1; // 队列为空,返回无效值
}
// 获取要出队的元素
// 删除元素,只能在队首进行
int dequeuedElement = queue->data[queue->front];
// 循环更新队列头指针
queue->front = (queue->front + 1) % queue->capacity;
// 更新元素个数
queue->size--;
return dequeuedElement; // 返回出队的元素
}
/**
* 释放队列内存
*
* 此函数用于释放队列所占用的内存,并重置队列的各个属性。
*
* @param queue 指向队列结构体的指针
*
* @return 无返回值
*
* @note
* - 如果传入的 `queue` 指针为 NULL,函数将输出错误信息并返回。
* - 该函数会释放动态数组内存,并将队列的各个属性重置为初始状态。
*/
void destroyQueue(Queue *queue)
{
if (queue == NULL)
{
puts("Error: 传入的队列为 NULL");
return;
}
free(queue->data); // 释放动态数组内存
queue->data = NULL; // 避免悬挂指针
queue->size = 0;
queue->capacity = 0;
queue->front = 0;
queue->rear = 0;
puts("Success:队列所占用的内存已成功释放^_^");
}
/**
* 遍历队列
*
* 此函数用于遍历队列中的所有元素并打印。
*
* @param queue 指向队列结构体的指针
*
* @return 无返回值
*
* @note
* - 如果传入的 `queue` 指针为 NULL,函数将输出错误信息并返回。
* - 该函数会从队列头指针开始,遍历队列中的所有元素并打印。
*/
void printQueue(Queue *queue)
{
if (queue == NULL)
{
puts("Error: 传入的队列为 NULL");
return; // 队列为空指针,退出函数
}
if (queue->size == 0)
{
puts("Warning: 队列为空队列,已无元素可遍历");
return; // 队列为空,退出函数
}
puts("开始遍历队列元素:");
// 不能使用下面这种行为进行遍历
// 循环队列的设计使得队列的头指针 front 和尾指针 rear 可能在数组中不是连续的。
// 具体来说,当 rear 达到数组末尾时,它会循环回到数组的起始位置。
// 因此,队列中的元素可能分布在数组的不同部分。
// for (int i = 0; i < queue->size; i++)
// {
// printf("%d", queue->data[i]);
// }
// 正确的做法是从 front 开始遍历,直到遍历完 size 个元素
for (int i = queue->front, j = 0; j < queue->size; i++, j++)
{
int data = queue->data[i % queue->capacity];
// i % queue->capacity 确保 i 始终在数组的有效索引范围内。
// 例如,假设 capacity 为 5,i 为 4,那么(4 + 1) % 5 的结果是 0,
// 这样 i 会从 4 变为 0,即数组的起始位置。 printf("%d ", data);
printf("%d ", data);
}
printf("\n");
}
int main()
{
Queue myQueue;
puts("----------------------1.初始化队列----------------------");
initQueue(&myQueue, 2);
initQueue(NULL, 2); // 错误测试
initQueue(&myQueue, 0); // 错误测试
puts("----------------------2.元素入队----------------------");
enqueue(&myQueue, 1);
enqueue(&myQueue, 2);
printf("队列中元素的个数为:%d\n", getSize(&myQueue));
enqueue(&myQueue, 3); // 满了,无法入队
enqueue(NULL, 4); // 错误测试
printf("队列中元素的个数为:%d\n", getSize(&myQueue));
puts("----------------------3.遍历队列----------------------");
printQueue(&myQueue); // 遍历队列
printQueue(NULL); // 错误测试
puts("----------------------4.元素出队----------------------");
printf("出队的元素是:%d\n", dequeue(&myQueue));
printf("出队的元素是:%d\n", dequeue(&myQueue));
printf("队列中元素的个数为:%d\n", getSize(&myQueue));
printQueue(&myQueue); // 遍历队列,没有元素可被遍历了
puts("----------------------4.释放队列内存----------------------");
destroyQueue(NULL); // 错误测试
destroyQueue(&myQueue);
return 0;
}
输出结果如下所示:
标签:顺序,队列,元素,NULL,queue,入队,size,指针 From: https://blog.csdn.net/qq_53139964/article/details/142979422