前言:队列中有一种特殊的存在——环形队列,其有一定的价值与意义,这篇文章主要由一道与其相关的例题来引出相关的知识内容。
注:下述解题过程是用C语言实现。
目录
一:题目简述
二:环形队列的简单介绍
三:环形队列的实现
(1) 数组实现
1. 过程分析
2. 代码实现
(2) 链表实现
1.过程分析
2. 代码实现
一:题目简述
这是来自leetcode中一道与队列相关的题,题目难度中等,这道题的实质是要实现满足循环队列的两个条件,要避免进入队列是循环状态的误区。
大家可以先自己做一做:力扣 622.设计循环队列
二:环形队列的简单介绍
队列中有一种特殊的结构——循环队列,在一些场景会进行使用,有一定的价值。
环形队列的主要特定有两个:
1. 与队列一样,数据遵循先进先出的原则——所以只能从队尾插入数据。
2. 环形队列的空间大小是一定的,也就是说它的空间可以重复利用(用数组实现时)。
注意:图示只是为了能够更直观的展现一种循环队列的感觉(且仅表示数组实现),实际上数组和链表实现起来依旧是用它们自已的的物理结构,只是通过条件控制满足了循环队列的条件,从而达到了设计循环队列的效果。
三:环形队列的实现
(1) 数组实现
由循环队列的特点我们知道循环队列的实现需要判断队列是否满了,而队列为空的判断条件是队头=队尾。用数组实现环形队列时,为了防止二者条件的重复,我们需要多开辟一个不属于队列的空间来表示队尾的位置(队尾数据的下一个),而这就是数组实现循环队列的关键之处。所以数组实现循环队列的要点就是当数组越界时再返回到数组的起始位置,即需要利用取模的思想控制数组的下标从而控制数组的边界,防止数组越界并实现循环的效果。
1. 过程分析
这里再强调一下:数组一定要多开辟一个空间来满足队列为满的条件的判断。
下面再分析几个特殊的边界情况:
(1) 队列为空时,head==tail
(2) 队列为满时,tail的下一个位置是head
数组中判断环形队列为满的条件是比较重要的一项,为了防止数组越界并恰当的判断队列为满,当(tail+1)%(数组空间大小)==head即可。
(3) tail位于数组尾部,继续插入数据 (队头删除数据head走到尾部时情况类似,也要防止越界)
(4) 获取队尾数据(红圈表示队尾数据)时,有两种情况:
特殊:当tail位于数组首位时,tail的前一位就不能表示队尾数据的下标
正常:tail不位于数组首位,tail的前一位的下标就可以表示队尾数据的下标
解决方法也有两种:直接分情况或利用取模思想获取队尾下标(下述代码中可见)。
2. 代码实现
//1. 用数组实现 (要点:利用取模的思想控制边界)
typedef struct
{
int* a;//动态开辟数组空间
int head;
int tail;//head与tail都表示数组的下标
int capacity;//表示队列的实际容量(可以表示数组最后一个位置的下标)
} MyCircularQueue;
bool myCircularQueueIsEmpty(MyCircularQueue* obj);
bool myCircularQueueIsFull(MyCircularQueue* obj);//声明
MyCircularQueue* myCircularQueueCreate(int k)//设置队列实际容量为k
{
MyCircularQueue* cq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
cq->a = (int*)malloc(sizeof(int) * (k + 1));//数组要多开辟一个空间,使区分出队列空与满两种情况
cq->head = cq->tail = 0;
cq->capacity = k;
return cq;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)//插入数据
{
if (myCircularQueueIsFull(obj))
{
return false;
}
else
{
obj->a[obj->tail] = value;
obj->tail++;
obj->tail %= (obj->capacity + 1);//向队尾插入数据时防止越界
return true;
}
}
bool myCircularQueueDeQueue(MyCircularQueue* obj)//删除数据
{
if (myCircularQueueIsEmpty(obj))
{
return false;
}
else
{
obj->head++;
obj->head %= (obj->capacity + 1);//防止越界
return true;
}
}
int myCircularQueueFront(MyCircularQueue* obj)//获取对头数据
{
if (myCircularQueueIsEmpty(obj))
{
return -1;
}
else
{
return obj->a[obj->head];
}
}
int myCircularQueueRear(MyCircularQueue* obj)//获取队尾数据
{
if (myCircularQueueIsEmpty(obj))
{
return -1;
}
//else 解决1(更直观)
//{
// if(obj->tail == 0) 当tail位于数组的起始位置时,tail-1不是队尾的值
// {
// return obj->a[obj->capacity];//直接在数组最后一个位置插入数据
// }
// else
// {
// return obj->a[obj->tail-1];
// }
//}
else //解决2(利用取模的思想)
{
//我自己实现时犯的一个错误
//obj->tail = (obj->tail+obj->capacity) % (obj->capacity+1);
//return obj->a[obj->tail]; err (这里只是要获取队尾数据,不能改变tail的位置)
int i = (obj->tail + obj->capacity) % (obj->capacity + 1);
return obj->a[i];
}
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj)//判断队列是否为空
{
return obj->head == obj->tail;
}
bool myCircularQueueIsFull(MyCircularQueue* obj)//判断队列是否为满
{
return (obj->tail + 1) % (obj->capacity + 1) == obj->head;
}
void myCircularQueueFree(MyCircularQueue* obj)//销毁队列
{
free(obj->a);
free(obj);
}
(2) 链表实现
1.过程分析
用链表实现循环队列实际上比数组简单一些,与数组不同的是我们可以通过计数的方式来判断队列是否为满,这样就不用开辟额外的空间。
计数方式主要表现在定义链表结构时可以多定义一个size变量来表示链表中的有效数据个数,当其等于设置好的队列空间大小时,就可以表示循环队列已满,而队列满了后也可以通过出列(删除队头元素)使size减小进而能够再次向队列中插入数据。
注意:由于链表存在循环结构,所以可能有些人会认为链表实现循环队列是通过循环链表来实现的,我这里特别说明一下,也许可以(我不知道如何实现),但是我这里实现是通过普通单链表尾插头删控制队列内元素个数的多少来实现的(与实现队列的思路相同)。并且我认为这种实现方式已经足够简单了。
2. 代码实现
//2.用单链表实现循环队列————可以想象成一个大小固定并且可以移动的队列
//用单链表实现循环队列时,可以用size与capacity来控制队列空间,当size<capacity时就可以插入数据
//中心思想:空间大小固定,队列满时不可再入数据,但是可以通过删除队头数据使队列空间不再是满的状态从而能够继续向队列内入数据,从而达到了一种循环队列的效果
//要注意:链表不是环状,只是达到了循环队列的条件!!!!!
typedef struct
{
struct ListNode* head;
struct ListNode* tail;
//设置size与capacity,以二者大小判断队列空间,从而判断能否插入数据(通过设置计数的方式可以判断队列空或者满)
int size;//有效数据个数
int capacity;//链表容量(也表示队列的容量)
} MyCircularQueue;
bool myCircularQueueIsEmpty(MyCircularQueue* obj);
bool myCircularQueueIsFull(MyCircularQueue* obj);//两个函数的声明
MyCircularQueue* myCircularQueueCreate(int k)//创建队列
{
MyCircularQueue* cq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));//队列
cq->head = cq->tail = NULL;
cq->size = 0;
cq->capacity = k;//给定队列容量为k
return cq;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)//入列
{
if (myCircularQueueIsFull(obj))//队列满了就不可再入数据
{
return false;
}
else
{
struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
newnode->val = value;
newnode->next = NULL;
if (obj->head == NULL)
{
obj->head = obj->tail = newnode;
}
else
{
obj->tail->next = newnode;
obj->tail = newnode;
}
obj->size++;
return true;
}
}
bool myCircularQueueDeQueue(MyCircularQueue* obj)//出列
{
if (myCircularQueueIsEmpty(obj))
{
return false;
}
else
{
struct ListNode* next = obj->head->next;
free(obj->head);
obj->size--;
obj->head = next;
return true;
}
}
int myCircularQueueFront(MyCircularQueue* obj)//获取队头数据
{
if (myCircularQueueIsEmpty(obj))
{
return -1;
}
return obj->head->val;
}
int myCircularQueueRear(MyCircularQueue* obj)//获取队尾数据
{
if (myCircularQueueIsEmpty(obj))
{
return -1;
}
return obj->tail->val;
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj)//判空
{
return obj->size == 0;
}
bool myCircularQueueIsFull(MyCircularQueue* obj)//判满
{
return obj->size == obj->capacity;
}
void myCircularQueueFree(MyCircularQueue* obj)//释放队列(相当于释放单链表)
{
struct ListNode* cur = obj->head;
while (cur)
{
struct ListNode* next = cur->next;
free(cur);
cur = next;
}
free(obj);
obj = NULL;
}
总结:
设计循环队列主要是基于这道题目,我个人认为链表实现起来更为简单,但是大家可以都掌握。如各位发现了文章的错误,还望指正,谢谢。就这样,这次的分享到这里结束,再次希望各位能有所收获,再见。