本题来自:622. 设计循环队列 - 力扣(LeetCode)
目录
题面:
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k)
: 构造器,设置队列长度为 k 。Front
: 从队首获取元素。如果队列为空,返回 -1 。Rear
: 获取队尾元素。如果队列为空,返回 -1 。enQueue(value)
: 向循环队列插入一个元素。如果成功插入则返回真。deQueue()
: 从循环队列中删除一个元素。如果成功删除则返回真。isEmpty()
: 检查循环队列是否为空。isFull()
: 检查循环队列是否已满。
示例:
MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3 circularQueue.enQueue(1); // 返回 true circularQueue.enQueue(2); // 返回 true circularQueue.enQueue(3); // 返回 true circularQueue.enQueue(4); // 返回 false,队列已满 circularQueue.Rear(); // 返回 3 circularQueue.isFull(); // 返回 true circularQueue.deQueue(); // 返回 true circularQueue.enQueue(4); // 返回 true circularQueue.Rear(); // 返回 4
提示:
- 所有的值都在 0 至 1000 的范围内;
- 操作数将在 1 至 1000 的范围内;
- 请不要使用内置的队列库。
代码:
#include <stdio.h>
#include <malloc.h>
typedef struct MyCircularQueue
{
int* _myqueue;
int _front;
int _tail;
int _size;
int _capacity;
} MyCircularQueue,mq;
MyCircularQueue* myCircularQueueCreate(int k)
{
mq* pq = (mq*)malloc(sizeof(mq)); // 先申请操作变量的空间
pq->_capacity = k; // 获取队列的大小
pq->_size = 0; // size记录当前队列元素的个数
pq->_myqueue = (int*)malloc(sizeof(int) * k); // 再申请环形队列的空间,这里用数组方法
pq->_front = pq->_tail = 0; // 让头尾下标归零
return pq; // 把这个环形队列打出去
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
if (obj->_size == obj->_capacity) // 对列中元素的数量和容量相等说明满了,弹出false
return false;
// 到这里说明对列不满
obj->_myqueue[obj->_tail] = value;// 把元素放到对列尾tail处,队尾后移
obj->_tail++;
if (obj->_tail == obj->_capacity) // tail下标超过了队列的大小
// 说明对列的最后一个空间有数,但是对列未满
obj->_tail = 0; // 让tail下标再指向对列空间的0地址处
obj->_size++;//对列个数+1
return true;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj)
{
if (obj->_size == 0) // 对列有效元素为空,无法删除
return false;
// 至少有一个元素
obj->_front++;
if (obj->_front == obj->_capacity) // 如果头下标也到了空间尾,并把空间尾删除了,下标指向了对列后面的空间,就把front指向0地址处
obj->_front = 0;
obj->_size--;// 对列个数-1
return true;
}
int myCircularQueueFront(MyCircularQueue* obj)
{
if (obj->_size == 0) // 队列为空,无法访问头对头
return -1;
return obj->_myqueue[obj->_front];
}
int myCircularQueueRear(MyCircularQueue* obj)
{
if (obj->_size == 0)
return -1;
if (obj->_tail != 0) // 如果tail下标为0,说明队列尾部已满,但是对头空间空出来了
return obj->_myqueue[obj->_tail - 1]; // tail下标始终指向的是实际队尾的下一个
else
return obj->_myqueue[obj->_capacity - 1]; // tail指向0地址处,就把最后一个地址的元素打出去
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj)
{
return !obj->_size; // 这里是队列元素的个数取反
}
bool myCircularQueueIsFull(MyCircularQueue* obj)
{
return obj->_size == obj->_capacity ? true : false; // 如果队列满了就返回true
}
void myCircularQueueFree(MyCircularQueue* obj)
{
free(obj->_myqueue); // 销毁队列
free(obj); // 再销毁指向队列的变量
}
/**
* Your MyCircularQueue struct will be instantiated and called as such:
* MyCircularQueue* obj = myCircularQueueCreate(k);
* bool param_1 = myCircularQueueEnQueue(obj, value);
* bool param_2 = myCircularQueueDeQueue(obj);
* int param_3 = myCircularQueueFront(obj);
* int param_4 = myCircularQueueRear(obj);
* bool param_5 = myCircularQueueIsEmpty(obj);
* bool param_6 = myCircularQueueIsFull(obj);
* myCircularQueueFree(obj);
*/
代码分析:
这道题吧,官方题解也写的非常乱,一些网课老师教的方法也过于麻烦。最常见的是申请k+1个节点的空间,用front和rear两个指针移动判断队列满不满啥的,还有这俩指针重合,是空是满的判断,总之就是特别麻烦。
所以,本人痛定思痛,果断舍弃了上面这种做法。那么,来讲讲我的方法吧。
队列通常有两种结构,一种是数组队列,一种是链表队列。
数组队列的优点是可以随机访问,缺点是空间固定,链表队列的优点就是大小不固定。
在这道题中,数组队列完美匹配了题意,每次给固定大小的k空间,那么我们只需要动态申请k个连续空间的数组交给一个数组指针来维护就可以了。
用人话说就是,题里给你这个k值,但是k值不固定,需要每次根据k的大小来申请空间。
所以,只能用一个指针来维护这个数组。在代码中,我用的是int*mequeue来维护空间。
这块儿的代码主要实现在queuecreate接口中。
正好讲一下,我设计结构中各成员的作用吧。
size负责记录当前队列中存了几个元素。
capacity来记录题给k值,也就是队列空间的大小。
front是对头元素所在的下标。
tail就是队尾元素所在的下标。
myqueue就是维护数组队列空间的指针。
在create接口中,主要是对循环队列的初始化,先给这个队列的各个参数申请空间,这样即使出了接口也能使用这些变量,然后给capacity赋值,size归零,再给myqueue这个指针申请k个int的空间,把front和tail指向0地址。
接下来是enqueue接口,这个接口最重要的也不为过,先捋一下逻辑,正常来说,我们往数组插入数据的方式是通过下标来访问和修改值的。这里用tail来确定插入的位置,只有一点比较特殊,当tail走到了这个空间的尾部时,要把tail归零,不管0地址处是否有效,先指过去,按照传统来说,一个数组的下标越界之后,只访问不做任何修改是不会出问题的,但是人家网站就是不允许,所以一旦数组下标越界之后,统统放到0地址处。这里我们把数据放到tail下标所在空间,当队列满的时候,就不能再放了直接弹出false就可以了。每次放数据之后,tail向后移动一步,这里用if判断是否越界,一旦越界就拉回来。没了。
还有一个能讲的接口是dequeue,这个接口是唯一一个可以删除元素的节点,当然删除元素的方式有很多种,有释放空间,还有越过的方式,这里的数组早晚都可以free,所以我们直接越过数据就好了,在上面的接口是tail可能越界,这里是front可能越界,处理方式和上面的一样,一旦front越界就拉回0。。。。。。。什么是front越界呢?这个接口就是删除对头,我们的做法就是front++,front++的时候就会越界。
哦,差点忘了还有一个接口可能越界,但是有了方法的改良之后,大概率是不会越界的,就是接口rear,这个接口明明没用对数组进行操作,但是这个接口确实曾经越界过,因为之前的时候,放数据和处理front的大小这个顺序颠倒了。我一直想的是,假如有3个数据吧,我们放了123进去,这时front是0,tail是3,我们访问的时候只访问数组偏移量为2的地方就好了,但是人家网站不允许这种方式,哪怕tail只是指向了数组后面的空间,不使用也不行。所以要及时处理tail和front的大小。如果越界就拉回0处。
剩下的接口都比较简单了,也没出过问题,就不讲了。
完。。。。。