目录
本节我们采用数组(顺序表)形式实现队列,学习单链表实现请点击下方链接:
为了减少数组初始长度过大对内存空间的浪费,本节我们采用动态内存管理,相关函数请的介绍点击下方链接:
动态内存函数(malloc,free,calloc,realloc)-CSDN博客
循环队列的实现:
?
队列是一种基本的数据结构,它是一种先进先出(First In First Out,FIFO)的线性结构。队列只允许在表的一端进行插入,而在另一端进行删除操作。这就相当于把数据排成一排,先插入的排在前面,后插入的排在后面,之后进行删除操作时也只能从前面依次删除。这种数据结构一般用于需要按照先后顺序进行处理的问题,如模拟系统、计算机网络中的缓存、操作系统中的进程调度等。队列的基本操作包括入队(插入元素到队尾)和出队(从队头删除元素),队列还有一个重要的特性就是队列的长度是动态变化的,随着入队和出队的操作进行不断变化。
???
长度固定:普通数组队列的长度是固定的,一旦数组被分配,其长度无法改变。当队列元素数量超过数组长度时,需要进行数组的扩容操作,这会导致性能上的开销。
内存的浪费:因为普通数组队列的长度固定,可能会出现队列中存在空闲的位置,导致内存的浪费。
为了解决上述 问题1,我们在本节中对顺序表采取动态内存管理,在必要时更新数组的长度,以保证顺序队列的长度足够使用。
?(补充:问题2?的解决需要使用循环队列,本节内容先为大家介绍一般队列的实现,等同学们对队列有了充分的理解之后,我们下节再进行循环队列的学习。)
一、我们首先定义一个数组,数组的头部为队首,尾部为队尾。每当插入一个元素时,就将元素放在队尾,当删除一个元素时,将队首的元素删除。当队列为空时,不能再删除元素。
二、我们采用双指针法时刻记录队列的队首和队尾:
定义一个固定大小的数组作为队列的存储空间,并定义两个指针front和rear分别指向队列的队首和队尾。
初始化队列时,将front和rear都设置为0,表示队列为空。
插入元素时,将元素放入rear指针指向的位置,并将rear指针后移一位。
删除元素时,将front指针后移一位。
判断队列是否为空,只需要判断front和rear是否相等即可。
初始化队列时,将front和rear都设置为0,表示队列为空。
typedef int DataType;
typedef struct Queue
{
DataType* a; // 队列的数组
int front, rear; // 队列的头部和尾部位置索引
int size; // 队列中元素的数量
int capacity; // 队列的容量
} Queue;
// 初始化队列
void InitQueue(Queue* q)
{
q->a = NULL; // 数组指针初始化为NULL
q->front = q->rear = 0; // 头部和尾部位置索引初始化为0
q->size = q->capacity = 0; // 元素数量和容量都初始化为0
}
// 入队
void QueuePush(Queue* q, DataType x)
{
assert(q); // 断言q不为NULL
if (q->capacity == q->rear)
{
// 如果队列已满,进行扩容操作
int new_capacity = q->capacity == 0 ? 10 : q->capacity * 2; // 扩容的大小为原容量的2倍
DataType* temp = (DataType*)realloc(q->a, new_capacity * sizeof(DataType)); // 重新分配内存空间
if (temp == NULL)
{
perror("realloc fail"); // 扩容失败,则输出错误信息
exit(-1); // 退出程序
}
q->capacity = new_capacity; // 更新队列的容量
q->a = temp; // 更新数组指针
}
q->a[q->rear++] = x; // 在尾部插入新元素,并更新尾部位置索引
q->size++; // 元素数量加1
}
判断队列是否为空,只需要判断front和rear是否相等即可。
// 判断队列是否为空
bool QueueEmpty(Queue* q)
{
assert(q); // 断言q不为NULL
if (q->front == q->rear)
{
return true; // 头部和尾部位置索引相等,队列为空
}
return false; // 队列不为空
}
?删除元素时,将front指针后移一位。
void QueuePop(Queue* q)
{
assert(q); // 断言q不为NULL
if (!QueueEmpty(q))
{
q->front++; // 更新头部位置索引
q->size--; // 元素数量减1
}
else
{
printf("队列已空,删除失败!\n"); // 队列为空,无法出队
}
}
// 获取队首元素
DataType QueueTop(Queue* q)
{
assert(q); // 断言q不为NULL
if (!QueueEmpty(q))
{
return q->a[q->front]; // 返回队首元素的值
}
else
{
printf("队列已空,获取队头元素失败!\n"); // 队列为空,无法获取队首元素
exit(-1); // 退出程序
}
}
队尾指针q->rear在最后一个元素的下一位,所以我们返回队尾元素时需要返回队尾坐标的前一个坐标所指向的元素。
// 获取队尾元素
DataType QueueTail(Queue* q)
{
assert(q); // 断言q不为NULL
if (!QueueEmpty(q))
{
return q->a[q->rear - 1]; // 返回队尾元素的值
}
else
{
printf("队列已空,获取队尾元素失败!\n"); // 队列为空,无法获取队尾元素
exit(-1); // 退出程序
}
}
// 获取队列中元素的数量
int QueueSize(Queue* q)
{
assert(q); // 断言q不为NULL
return q->size; // 返回元素数量
}
// 销毁队列
void QueueDestory(Queue* q)
{
assert(q); // 断言q不为NULL
free(q->a); // 释放队列的数组空间
q->a = NULL; // 数组指针置为NULL
}
?通过对顺序队列的学习我们可以明显看到顺序队列的缺点。当我们删除队首元素后,由于队列只能从队尾进行增加元素的操作,所以front指针之前的空间不能再进行使用。
如果是在队列长度是固定长度的情况下,当队尾指针rear到达最大时,队列已满,数组内已经没有空间进行插入操作,但由于此时front指针前可能还有空余空间,这时我们就造成了空间的浪费。
我们把这种现象称为“假溢出”现象。那么通过数组的循环队列或者链队列我们可以很好的解决此类现象。
下节我们将对如何用数组实现循环队列进行介绍:循环队列(数组实现)-CSDN博客
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int DataType;
typedef struct Queue
{
DataType* a; // 队列的数组
int front, rear; // 队列的头部和尾部位置索引
int size; // 队列中元素的数量
int capacity; // 队列的容量
} Queue;
// 初始化队列
void InitQueue(Queue* q)
{
q->a = NULL; // 数组指针初始化为NULL
q->front = q->rear = 0; // 头部和尾部位置索引初始化为0
q->size = q->capacity = 0; // 元素数量和容量都初始化为0
}
// 判断队列是否为空
bool QueueEmpty(Queue* q)
{
assert(q); // 断言q不为NULL
if (q->front == q->rear)
{
return true; // 头部和尾部位置索引相等,队列为空
}
return false; // 队列不为空
}
// 入队
void QueuePush(Queue* q, DataType x)
{
assert(q); // 断言q不为NULL
if (q->capacity == q->rear)
{
// 如果队列已满,进行扩容操作
int new_capacity = q->capacity == 0 ? 10 : q->capacity * 2; // 扩容的大小为原容量的2倍
DataType* temp = (DataType*)realloc(q->a, new_capacity * sizeof(DataType)); // 重新分配内存空间
if (temp == NULL)
{
perror("realloc fail"); // 扩容失败,则输出错误信息
exit(-1); // 退出程序
}
q->capacity = new_capacity; // 更新队列的容量
q->a = temp; // 更新数组指针
}
q->a[q->rear++] = x; // 在尾部插入新元素,并更新尾部位置索引
q->size++; // 元素数量加1
}
// 出队
void QueuePop(Queue* q)
{
assert(q); // 断言q不为NULL
if (!QueueEmpty(q))
{
q->front++; // 更新头部位置索引
q->size--; // 元素数量减1
}
else
{
printf("队列已空,删除失败!\n"); // 队列为空,无法出队
}
}
// 获取队首元素
DataType QueueTop(Queue* q)
{
assert(q); // 断言q不为NULL
if (!QueueEmpty(q))
{
return q->a[q->front]; // 返回队首元素的值
}
else
{
printf("队列已空,获取队头元素失败!\n"); // 队列为空,无法获取队首元素
exit(-1); // 退出程序
}
}
// 获取队尾元素
DataType QueueTail(Queue* q)
{
assert(q); // 断言q不为NULL
if (!QueueEmpty(q))
{
return q->a[q->rear - 1]; // 返回队尾元素的值
}
else
{
printf("队列已空,获取队尾元素失败!\n"); // 队列为空,无法获取队尾元素
exit(-1); // 退出程序
}
}
// 获取队列中元素的数量
int QueueSize(Queue* q)
{
assert(q); // 断言q不为NULL
return q->size; // 返回元素数量
}
// 销毁队列
void QueueDestory(Queue* q)
{
assert(q); // 断言q不为NULL
free(q->a); // 释放队列的数组空间
q->a = NULL; // 数组指针置为NULL
}
int main()
{
Queue q;
InitQueue(&q);
QueuePush(&q, 5);
QueuePush(&q, 6);
QueuePush(&q, 7);
QueuePush(&q, 8);
QueuePush(&q, 9);
QueuePush(&q, 10);
DataType x;
x = QueueTop(&q);
printf("%d\n", x);
x = QueueTail(&q);
printf("%d\n", x);
QueuePop(&q);
QueuePop(&q);
QueuePop(&q);
QueuePop(&q);
QueuePop(&q);
x = QueueTop(&q);
printf("%d\n", x);
x = QueueSize(&q);
printf("%d\n", x);
QueueDestory(&q);
return 0;
}
由于固定长度队列无需扩容,所以不需要进行动态内存的分配,也不需要进行销毁队列的操作。
同时相对于动态增长的队列,固定长度的队列需要判断队内元素数量是否达到了队列的最大容量。由于我们在代码中是先对队尾指针rear指向的位置添加元素,再对rear进行自增,更新队尾索引,所以在本代码中队满的判断条件是rear==MAXLEN。
当对固定长度队列添加元素时,如果当前队列队尾指针已达到数组长度,由于队列只能从队尾添加元素,此时我们不能再为队列添加新的元素。所以在我们为队尾添加元素时,我们首先要判断队列是否已满——即队尾指针是否达到数组容量的最大值。
//判断队列是否为满
bool QueueFull(Queue* q)
{
assert(q); // 断言q不为NULL
if (q->rear == MAXLEN)
{
return true; // 尾部位置达到数组长度最大值,队列为满
}
return false; // 队列不为满
}
明白了以上几点,我们对动态增长队列的代码稍作修改,添加判断队列是否已满的函数并对增加队列元素作出限制,就可得到固定长度队列的代码。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#define MAXLEN 10
typedef int DataType;
typedef struct Queue
{
DataType a[MAXLEN]; // 队列的数组
int front, rear; // 队列的头部和尾部位置索引
int size; // 队列中元素的数量
} Queue;
// 初始化队列
void InitQueue(Queue* q)
{
assert(q);
q->front = q->rear = 0; // 头部和尾部位置索引初始化为0
q->size = 0; // 元素数量初始化为0
}
// 判断队列是否为空
bool QueueEmpty(Queue* q)
{
assert(q); // 断言q不为NULL
if (q->front == q->rear)
{
return true; // 头部和尾部位置索引相等,队列为空
}
return false; // 队列不为空
}
//判断队列是否为满
bool QueueFull(Queue* q)
{
assert(q); // 断言q不为NULL
if (q->rear == MAXLEN)
{
return true; // 尾部位置达到数组长度最大值,队列为满
}
return false; // 队列不为满
}
// 入队
void QueuePush(Queue* q, DataType x)
{
assert(q); // 断言q不为NULL
if (!QueueFull(q))//判断队列是否为满
{
q->a[q->rear++] = x; // 在尾部插入新元素,并更新尾部位置索引
q->size++; // 元素数量加1
}
else
{
printf("队列已满\n");
exit(-1);
}
}
// 出队
void QueuePop(Queue* q)
{
assert(q); // 断言q不为NULL
if (!QueueEmpty(q))
{
q->front++; // 更新头部位置索引
q->size--; // 元素数量减1
}
else
{
printf("队列已空,删除失败!\n"); // 队列为空,无法出队
}
}
// 获取队首元素
DataType QueueTop(Queue* q)
{
assert(q); // 断言q不为NULL
if (!QueueEmpty(q))
{
return q->a[q->front]; // 返回队首元素的值
}
else
{
printf("队列已空,获取队头元素失败!\n"); // 队列为空,无法获取队首元素
exit(-1); // 退出程序
}
}
// 获取队尾元素
DataType QueueTail(Queue* q)
{
assert(q); // 断言q不为NULL
if (!QueueEmpty(q))
{
return q->a[q->rear - 1]; // 返回队尾元素的值
}
else
{
printf("队列已空,获取队尾元素失败!\n"); // 队列为空,无法获取队尾元素
exit(-1); // 退出程序
}
}
// 获取队列中元素的数量
int QueueSize(Queue* q)
{
assert(q); // 断言q不为NULL
return q->size; // 返回元素数量
}
int main()
{
Queue q;
InitQueue(&q);
QueuePush(&q, 5);
QueuePush(&q, 6);
QueuePush(&q, 7);
QueuePush(&q, 8);
QueuePush(&q, 9);
QueuePush(&q, 10);
QueuePush(&q, 5);
QueuePush(&q, 6);
QueuePush(&q, 7);
QueuePush(&q, 8);
//QueuePush(&q, 9);
//QueuePush(&q, 10);
DataType x;
x = QueueTop(&q);
printf("%d\n", x);
x = QueueTail(&q);
printf("%d\n", x);
QueuePop(&q);
QueuePop(&q);
QueuePop(&q);
QueuePop(&q);
QueuePop(&q);
x = QueueTop(&q);
printf("%d\n", x);
x = QueueSize(&q);
printf("%d\n", x);
return 0;
}
如果有同学在部分地方有疑惑,欢迎评论区讨论。
本节内容告一段落,我们下节博客见。