hello,大家好啊,肖恩又拖更了,你们听我狡辩,前段时间有期中考试,so我就没什么时间写这个,在这给大家道个歉😭😭😭
我后面一定尽力不拖更
那么接下来,我们来看队列这个数据结构
引言
队列是另一种常见的数据结构,它遵循"先进先出"(FIFO)的原则。队列通常用于存储等待处理的任务或数据,如任务调度、资源分配等。了解队列的基本概念和实现方式对于掌握算法和数据结构同样很重要。
队列的概念及结构
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出
FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头
队列的实现
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数
组头上出数据,效率会比较低。
那我们下面来实现队列的一些操作
因为也不是很难,我直接附上代码
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType val;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
//初始化
void QueueInit(Queue* pq) {
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
//销毁
void QueueDestroy(Queue* pq) {
assert(pq);
QNode* cur = pq->phead;
while (cur) {
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail == NULL;
pq->size = 0;
}
// 队尾插入
void QueuePush(Queue* pq, QDataType x) {
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL){
perror("malloc fail");
return;
}
newnode->next = NULL;
newnode->val = x;
if (pq->phead == NULL)
pq->phead = pq->ptail = newnode;
else
pq->ptail->next = newnode;
pq->ptail = newnode;
pq->size++;
}
// 队头删除
void QueuePop(Queue* pq) {
assert(pq);
assert(pq->size != 0);
if (pq->phead->next = NULL) {
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else {
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--;
}
// 取队头和队尾的数据
QDataType QueueFront(Queue* pq) {
assert(pq && pq->phead);
return pq->phead->val;
}
QDataType QueueBack(Queue* pq) {
assert(pq && pq->ptail);
return pq->ptail->val;
}
// 获取队列中有效元素个数
int QueueSize(Queue* pq) {
assert(pq);
return pq->size;
}
// 检测队列是否为空
bool QueueEmpty(Queue* pq) {
assert(pq);
return pq->size == 0;
}
简单说两个主要的操作
-
队尾插入 (QueuePush):
- 首先检查传入的队列指针是否为空,如果为空则抛出错误。
- 分配一个新的节点内存空间。
- 将新节点的
next
指针设置为NULL
,并将待插入的数据x
存储到节点的val
字段。 - 如果队列为空,则将新节点同时设置为头指针
phead
和尾指针ptail
。 - 否则,将当前尾节点的
next
指针指向新节点,并更新尾指针ptail
指向新节点。 - 最后将队列大小
size
加 1。
-
队头删除 (QueuePop):
- 首先检查传入的队列指针是否为空,如果为空则抛出错误。
- 检查队列是否为空,如果为空则什么也不做直接返回。
- 如果队列只有一个节点,则释放该节点并将头尾指针都设置为
NULL
。 - 否则,获取头节点的下一个节点,释放头节点,并将头指针
phead
指向下一个节点。 - 最后将队列大小
size
减 1。
通过这两个基本操作,我们可以实现一个基于链表的队列数据结构,支持向队列尾部添加元素和从队列头部删除元素的功能。
实际中我们有时还会使用一种队列叫循环队列。环形队列可以使用数组实现,也可以使用循环链表实现。
使用循环队列的优点是可以高效地利用数组空间,避免了普通队列在删除元素时,需要移动所有剩余元素的问题。但同时也需要额外维护头尾指针,增加了一定的复杂度。
循环队列在某些场景下,如缓存管理、生产者-消费者模型等,能够发挥出较好的性能表现。
那么下面的一个面试题(也是考研题)就让我们来看看循环队列。
例题
设计循环队列
使用动态数组很方便
我相信下面详细的注释能让你明白循环队列是怎么回事,不是很理解的话,可以自己画图来便于理解
// 定义循环队列结构体
typedef struct {
int *a; // 存储队列元素的动态数组
int head; // 队头下标
int tail; // 队尾下标
int k; // 队列最大容量
} MyCircularQueue;
// 检查队列是否为空
bool myCircularQueueIsEmpty(MyCircularQueue *obj) {
return obj->head == obj->tail; // 当 head 和 tail 相等时,队列为空
}
// 检查队列是否已满
bool myCircularQueueIsFull(MyCircularQueue *obj) {
return (obj->tail + 1) % (obj->k + 1) == obj->head; // 当 (tail + 1) % (k + 1) 等于 head 时,队列已满
}
// 创建一个容量为 k 的循环队列
MyCircularQueue *myCircularQueueCreate(int k) {
MyCircularQueue *p = (MyCircularQueue *)malloc(sizeof(MyCircularQueue)); // 分配 MyCircularQueue 结构体的内存
p->a = (int *)malloc((k + 1) * sizeof(int)); // 分配存储元素的动态数组
p->k = k; // 设置队列最大容量
p->head = p->tail = 0; // 初始化 head 和 tail 为 0
return p;
}
// 向队列中添加一个元素
bool myCircularQueueEnQueue(MyCircularQueue *obj, int value) {
if (myCircularQueueIsFull(obj)) // 如果队列已满,返回 false
return false;
obj->a[obj->tail] = value; // 将元素添加到队尾
obj->tail = (obj->tail + 1) % (obj->k + 1); // 更新 tail 下标,实现循环
return true;
}
// 从队列中删除一个元素
bool myCircularQueueDeQueue(MyCircularQueue *obj) {
if (myCircularQueueIsEmpty(obj)) { // 如果队列为空,返回 false
return false;
} else {
obj->head = (obj->head + 1) % (obj->k + 1); // 更新 head 下标,实现删除队头元素
return true;
}
}
// 获取队头元素
int myCircularQueueFront(MyCircularQueue *obj) {
if (obj->tail == obj->head) { // 如果队列为空,返回 -1
return -1;
} else {
return obj->a[obj->head]; // 返回队头元素
}
}
// 获取队尾元素
int myCircularQueueRear(MyCircularQueue *obj) {
if (obj->tail == obj->head) { // 如果队列为空,返回 -1
return -1;
} else {
return obj->a[(obj->tail + obj->k) % (obj->k + 1)]; // 返回队尾元素
}
}
// 释放循环队列占用的内存空间
void myCircularQueueFree(MyCircularQueue *obj) {
free(obj->a); // 释放存储元素的动态数组
free(obj); // 释放 MyCircularQueue 结构体
}
循环队列的存储空间为 Q(1:100) ,初始状态为 front=rear=100 。经过一系列正常的入队与退队操作
后, front=rear=99 ,则循环队列中的元素个数为( )
A. 1
B. 2
C. 99
D. 0或者100
现有一循环队列,其队头指针为front,队尾指针为rear;循环队列长度为N。其队内有效长度为?(假设
队头不存放数据)
A. (rear - front + N) % N + 1
B. (rear - front + N) % N
C. (rear - front) % (N + 1)
D. (rear - front + N) % (N - 1)
解析:
在循环队列中,队头指针 front 指向队首元素的下一个位置(如果队头不存放数据),而队尾指针 rear 指向队尾元素的下一个位置。这意味着当队列为空时,front 和 rear 通常指向同一个位置(或者在某些实现中,rear 可能在 front 的下一个位置,具体取决于如何定义“空”状态)。
队列的有效长度(即队列中元素的数量)可以通过计算 rear 和 front 之间的差值来得到,但是需要注意这是一个循环结构,所以差值需要用模运算 % N 来处理。
假设 front 和 rear 的初始值都是 0(队列为空),并且每次入队操作 rear 会递增,每次出队操作 front 会递增,那么队列的有效长度可以用以下公式计算:
(rear - front + N) % N
这里加 N 是为了确保在 rear 小于 front 的情况下(即队列绕了一圈之后),差值仍然是正数。然后取模 N 来得到一个 0 到 N-1 之间的值,这个值就是队列的有效长度。
注意:这个公式假设 front 和 rear 的初始值都是 0,并且每次操作后都会递增。如果初始值不是 0,或者有其他特殊的入队/出队逻辑,那么可能需要调整这个公式。
那么到这里,基本上队列的东西都讲完啦
下一篇讲队列和栈的相互实现~~~
待会儿见