数据结构——队列(附环形队列问题)

在之前的文章中,我们介绍了栈的基本概念与用法,栈是遵循先进后出的原则,而今天我们要介绍的队列,则是栈的相反概念,即先进先出,最后将具体介绍环形队列问题,希望这篇文章可以帮助到你。

目录

队列的概念及结构

队列的链表节点

队列的结构

队列函数

1.初始化队列

2.队尾入队列

3. 队头出队列

4.获取队列头部元素

5.获得队列队尾元素

6.获得队列有效个数

7. 检测队列是否为空,如果为空返回非零结果,如果非空返回0

 8.销毁队列

环形队列问题

题目:

思路:

解题:

初始化环形队列

判断是否为空

判断是否为满

插入和删除

返回头节点

返回尾节点

销毁环形链表

总结

队列的概念及结构

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头

队列在现实生活中应用广泛,如在银行的抽号机,先来的人会得到一个号码,然后在办理业务的时候,按照号码进行叫人,完成一个业务就继续往下实现。

队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低

数组在数组头上出数据时,必须让后面的元素依次向前挪动,因此效率不如链表。

我们建立队列的链表节点,如下

队列的链表节点

typedef struct QListNode
{ 
 struct QListNode* _pNext; 
 QDataType _data; 
}QNode; 

 而如果我们要实现队列的入队和出队,则需要找到队列的首和尾,如果每次都用遍历链表的方法来寻找尾节点,那么效率就比较低了,所以我们需要在函数中传入三个变量

(头节点,尾节点,数据)

那我们就可以想到将头节点和尾节点封装为一个结构体,这样只需要传入结构体就能代替两个变量,让函数参数更加简洁

由于我们之后还要实现队列元素的个数的函数,所以不妨我们再在结构体中加上size变量来记录队列元素的个数,之后实现函数也更加方便

因此我们创建队列的结构

队列的结构

​
typedef struct Queue
{ 
 QNode* _front; 
 QNode* _rear; 
 int size;
}Queue; 

​

下面我们就来实现队列的基本函数

队列函数

// 初始化队列
void QueueInit(Queue* q); 
// 队尾入队列
void QueuePush(Queue* q, QDataType data); 
// 队头出队列
void QueuePop(Queue* q); 
// 获取队列头部元素
QDataType QueueFront(Queue* q); 
// 获取队列队尾元素
QDataType QueueBack(Queue* q); 
// 获取队列中有效元素个数
int QueueSize(Queue* q); 
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q); 
// 销毁队列
void QueueDestroy(Queue* q);

队列函数和栈函数的用法大差不差,因此我们只需要效仿栈的函数实现即可

1.初始化队列

初始化队列,我们仍然只需要将对应的变量置为对应的空值即可

代码如下

void QueueInit(Queue* pq)
{
	assert(pq);
	pq->phead = NULL;
	pq->ptail = NULL;
	pq->size = 0;
}
2.队尾入队列

入队列即插入数据,对应链表的知识就是尾插的实现,因此我们需要用malloc开辟新节点,注意要检查是否开辟成功,若不成功就及时的中断代码。

插入的时候分为两种情况,第一种队列为空,即head为空时,那么就需要将head和tail都指向新节点newnode,另一种即正常的情况,我们只需要将新节点插入到tail的后面,再将tail向后移即可,不要忘记将size++;

这就是我们之前队列结构设计的好处,有head和tail以及size就省去了许多冗杂的代码,可以清晰的实现

void QueuePush(Queue* q, QDataType data)
{
	assert(q);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}
	newnode->val = data;
	newnode->next = NULL;
	if (q->phead == NULL)
	{
		q->phead = q->ptail = newnode;
	}
	else
	{
		q->ptail->next = newnode;
		q->ptail = newnode;
	}
	q->size++;
}
3. 队头出队列

出队列即头删的实现,我们首先需要用断言判断队列是否为空,若为空就中断代码的运行,然后将出队列也分为两种情况

第一种情况,只有以一个数据,即head的next为空,这时tail = head,所以我们在free头节点的之后,要记者把tail也置为空,否则会出现野指针的使用

第二种情况,即正常的情况,由于head节点需要释放,需要记录head的下一个位置,只需要新建一个next变量赋值为head的next ,然后free头节点head,再将next作为新的头即可

最后不要忘了将size--;

代码如下:

void QueuePop(Queue* q)
{
	assert(q);
	assert(q->size != 0);
	if (q->phead->next == NULL)
	{
		free(q->phead);
		q->ptail = q->phead = NULL;
	}
	else
	{
		QNode* next = q->phead->next;
		free(q->phead);
		q->phead = next;
	}
	q->size--;
}
4.获取队列头部元素

和获得栈顶元素类似,先确保head不为空,再返回值即可,不作过多解释

代码如下:

QDataType QueueFront(Queue* q)
{
	assert(q);
	assert(q->phead);
	return q->phead->val;
}
5.获得队列队尾元素

这个函数的实现,就得益于之前队列结构体的建立,有tail变量,可以轻松的获得尾节点,

如果没有设置tail变量就需要依次遍历链表来实现了

代码如下:

QDataType QueueBack(Queue* q)
{
	assert(q);
	assert(q->phead);
	return q->ptail->val;
}
6.获得队列有效个数

直接return size

代码如下:

int QueueSize(Queue* q)
{
	assert(q);
	return q->size;
}
7. 检测队列是否为空,如果为空返回非零结果,如果非空返回0

用size==0来判断队列是否为空

代码如下:

bool QueueEmpty(Queue* q)
{
	assert(q);
	return q->size == 0;
}
 8.销毁队列

销毁队列和单链表的销毁类似,建立cur节点从头节点开始遍历,然后依次释放开辟的节点空间,完成队列的销毁,最后将队列元素各值置空即可。

void QueueDestroy(Queue* q)
{
	assert(q);
	QNode* cur = q->phead;
	while (cur)
	{
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}
	q->phead = NULL;
	q->ptail = NULL;
	q->size = 0;
}

以上即队列基本函数的实现,逻辑和代码上都比较简单,希望大家能够掌握 

下面我们拓展一下环形队列问题

环形队列问题

题目:

下面是要实现的函数接口


typedef struct {
    
} MyCircularQueue;


MyCircularQueue* myCircularQueueCreate(int k) {
    
}

bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    
}

bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    
}

int myCircularQueueFront(MyCircularQueue* obj) {
    
}

int myCircularQueueRear(MyCircularQueue* obj) {
    
}

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    
}

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    
}

void myCircularQueueFree(MyCircularQueue* obj) {
    
}

思路:

这个题的意思是,空间大小是固定的,在有限的空间种保证先进先出

那么这道题我们就有两种选择,一种是用链表,一种是用数组

我们分析一下题目可知,由于环形队列是循环的,即最后一个元素的下一个位置就是开头的位置,那么我们要么用双向链表,要么用数组,这样实现是更为合理的,而数组更为方便,能够直接用下标进行访问,所以我们选择数组实现,具体结构如图所示

我们设置head,tail来记录头节点和尾节点的位置

当push数据的时候就push到tail节点的位置,然后让tail+1,到末尾的时候就返回下标0的位置重新开始,效果如图

pop的时候也是一样的,pop删除head位置的数据,然后令head++,走到尾的位置时,就返回下标尾0的位置重新开始

结构如图所示

 如果上面的思路能够理解的话,那这时候我们需要关注一个问题,什么时候是环形队列满的状态,什么时候是环形链表空的状态呢?

我们发现当head == tail的时候,既是满也是空,那么问题就出现了,我们不可能让同一个条件来表示两种状态,这样会混淆的,那我们该怎么办呢?

我们有两种解决方法

1.在结构体中加入一个size变量,来判断为空还是为满

2.额外多开辟一个空间

我们这里用第二个方法来解决问题

如图所示

当head == tail则为空

tail+1 = head或者当tail = k&&head == 0时为满

或者利用(tail+1)%(k+1)==head 来判断为满

这个公式需要结合图好好理解一下,非常的神奇

解题:

思路明确我们来实现一下题目代码

先建立结构体,头head,尾tail,空间a,空间大小k

typedef struct {
    int* a;
    int head;
    int tail;
    int k;
} MyCircularQueue;
初始化环形队列
MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    obj->a = (int*)malloc(sizeof(int)*(k+1));
    obj->head = 0;
    obj->tail = 0;
    obj->k = k;
    return obj;
}
判断是否为空

按照我们上面的思路head==tail时即为空

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return obj->head == obj->tail;
}
判断是否为满

直接利用神奇公式

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return (obj->tail+1)%(obj->k+1)==obj->head;
}
插入和删除

插入删除,和我们分析的一样,用tail插入,用head删除,要注意的是

1.要先判断是否为空或是否为满,避免出现错误

2.在tail++或者head++的最后要%k+1,避免tail或者head超出空间下标

bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    if(myCircularQueueIsFull(obj))
    return false;
    else
    obj->a[obj->tail] = value;
    obj->tail++;
    obj->tail%=(obj->k+1);
    return true;
}

bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
    return false;
    else
    obj->head++;
    obj->head%=(obj->k+1);
    return true;

}
返回头节点

这个直接返回head下标对应的数据即可

int myCircularQueueFront(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
    return -1;
    return obj->a[obj->head];
}
返回尾节点

返回尾节点,要注意,由于之前插入时tail在插入完成后+1,所以我们要返回tail-1位置的数据,而tail有可能在开头,那么我们就需要返回数组末的数据,直接三目操作符实现

int myCircularQueueRear(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
    return -1;
    return obj->tail==0?obj->a[obj->k]:obj->a[obj->tail-1];
}
销毁环形链表

由于是malloc开的数组空间,直接free即可

void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->a);
    free(obj);
}

总结

以上就是队列的基本用法和环形链表的实现,相对来说并不难,逻辑和代码的实现都比较清晰简单,希望大家能够掌握队列的知识。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值