【初阶数据结构和算法】leetcode刷题之设计循环队列
文章目录
- 一、实现循环队列
- 1.大致思路分析
- 2.循环队列的结构定义和初始化
- 结构定义
- 初始化
- 3.循环队列的判空和判满
- 判空和判满难点分析
- 判空
- 判满
- 4.循环队列的入队列和出队列
- 入队列
- 出队列
- 5.循环队列取队头和队尾元素
- 取队头元素
- 取队尾元素
- 6.循环队列的销毁
- 7.最后题解源码
一、实现循环队列
题目链接:https://leetcode.cn/problems/design-circular-queue/description/
1.大致思路分析
我们来看看题目描述:
这是leetcode上的一道中等难度的题,一般来说,leetcode上简单的题不一定简单,中等一定很麻烦,这道题虽然核心也不难,但是就是麻烦
在做这道题之前,我们要先来介绍一下什么是循环队列,循环队列是一种特殊的队列,它的首尾在逻辑上是相连的,但是还是队头出数据,队尾入数据,保持队列先进先出的特性,被称为环形缓冲器
但是它和我们之前实现的队列的最大不同是,之前我们实现的队列如果容量不够是可以扩容的,是可变的,而循环队列的容量确定之后就不能再更改,比如后面我们写初始化方法时,会给定一个值给我们初始化,然后我们开辟好空间后就不再修改它的大小了,所以循环队列的容量一经确定就不会再更改
那么我们接下来大致来分析一下循环队列的结构,我们是继续使用实现队列时的链表,还是说使用数组呢?其实两个都可以,只是使用链表需要使用循环链表,那么哪一个结构更优呢?
循环队列和普通队列的一个较大区别是,循环队列的大小是固定好了的,当我们去删除数据时,不会直接释放空间,而是会留着存储以后新插入的数据,那么很明显数组的优势更大一些
因为数组数据的删除可以只改变size,比如尾删就是让size- -,这样就间接实现了删除,我们访问不到原本的尾数据了,但是其实那个空间还在,只是我们size- -后访问不到了而已
但是链表数据的删除会释放节点,如果不释放节点的话让它强行留下来就会很麻烦,但是我们循环队列的大小又是固定的,到时候重新申请节点插入到原位置又很麻烦,并且链表的开销比数组要大,所以综上我们实现循环链表最好使用数组
由于队列要遵循头删尾入,所以我们最好像定义队列一样,定义两个下标front和rear分别指向循环队列的头和尾,方便进行操作,注意:rear和之前顺序表的size一样,不是指向有效元素中的最后一个元素,而是这个最后一个有效元素的下一个位置
那么数组怎么实现首尾相连呢?首尾相连就是尾的下一个节点是头,实现这个需要一定的技巧性,要使用模运算,当rear或者front越界时,模上容量大小就可以让它们重新回到开头,就像首尾相连了一样,如图:
现在我们大致知道了循环队列的底层存储,接下来就边做题边讲解思路
2.循环队列的结构定义和初始化
结构定义
现在我们知道了使用数组,所以循环队列中肯定要有一个数组,但是我们不知道数组的具体大小,需要到时候根据初始化函数的参数确定,所以我们这里直接用一个指针代替,到时候动态申请空间
由于队列要遵循头删尾入,所以我们最好像定义队列一样,定义两个下标front和rear分别指向循环队列的头和尾(这个尾相当于顺序表中的size),方便进行操作,还有就是我们要知道申请了多大空间,所以用一个整型变量capacity来存储
那么是否要头删使用数组的效率会很低呢?这个是不需要担心的,因为循环队列的空间不需要释放,所以头删只需要让front++即可,到后面出队列再细讲,那么最后循环队列的大致结构定义如下:
typedef struct
{
int* arr;
int capacity;
int rear;
int front;
} MyCircularQueue;
初始化
在初始化函数中给了我们一个参数k,就是我们要申请的空间大小,我们直接通过动态申请k个整型(后面可能会需要更改,我们先实现到这里),然后将容量置为k,两个指向头和尾的下标置为0,如下:
MyCircularQueue* myCircularQueueCreate(int k)
{
MyCircularQueue* pq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
if(pq == NULL)
{
perror("malloc");
return NULL;
}
//后面这里可能会进行改动,这里做一个标记
pq->arr = (int*)malloc(sizeof(int) * k);
if(pq->arr == NULL)
{
perror("malloc");
return NULL;
}
pq->rear = pq->front = 0;
pq->capacity = k;
return pq;
}
3.循环队列的判空和判满
判空和判满难点分析
循环队列的难点不是后面的入队列和出队列,而是这个部分的判空和判满,因为我们会发现队列空时front和rear相等,队列满时front和rear也相等了,它们难以区分了,如图:
可以看到队列空时,front和rear相等,指向头部,那么如果队列满了呢?如图:
这里的rear相当于是之前顺序表中的size,指向有效数据的下一个位置,那么现在队列满了,rear指向元素5的下一个位置,同时元素5是最后一个元素,所以根据循环首尾相连的特点,它需要指向头
那么怎么让它回到头呢?采用的方法就是让rear%capacity,这个方法可以自动帮我们去将越界的rear放回到头,但是问题就来了,此时front和rear又相等了,那么就说明front和rear相等可能是队列为空,也可能是队列满了
怎么解决呢?我们这样做,在开辟空间时,我们不再开辟k个空间,而是开辟k+1个空间,多开辟一个空间,有什么作用呢?我们画画图就知道了:
是不是非常巧妙呢?但是问题又来了,当空间满了之后,front和rear不相等了,我们如何判断队列是否满了呢?
其实也不难,我们多开了一个空间,那么实际的空间就应该是capacity+1,实际rear也要+1,所以判断循环队列空间是否为满的条件就是front等于(rear+1) % (capacity+1),现在我们简单画个图来解释,后面还有其它情况到时候再说,如下:
那么现在我们分析好了之后,我们还要做一件事,就是之前我们只开辟了k个空间,现在我们要把它改成k+1个空间,其它不变,如下:
pq->arr = (int*)malloc(sizeof(int) * (k+1));
这个做完之后我们就可以直接来实现判空和判满了
判空
判空的条件就是循环队列的front等于rear,如下:
bool myCircularQueueIsEmpty(MyCircularQueue* obj)
{
return obj->front == obj->rear;
}
判满
判满的条件就是循环队列 (rear + 1) % (capacity + 1) == front,如下:
bool myCircularQueueIsFull(MyCircularQueue* obj)
{
return (obj->rear + 1) % (obj->capacity + 1) == obj->front;
}
4.循环队列的入队列和出队列
我们先来看看题目给出入队列和出队列操作的要求,如图:
题目的意思是如果我们插入或删除成功需要返回true,虽然题目没有说,但是我们应该都能想到插入和失败是不是就要返回false了,所以我们在入队列和出队列之前要记得判断能否插入和删除数据,那么接下来我们进入对队列和出队列的具体分析
入队列
在入队列之前,我们首先要判断循环队列能否入队列,如果它满了,说明我们不能再插入数据了,就要直接返回false
判断完之后我们就要真正来实现入队列操作了,入队列就是往数组的最后插入数据,也就是往rear的位置插入数据,插入完之后让rear++
但是我们要注意一个点,rear++之后可能会越界,我们要让它模上数组真正大小,也就是capacity+1,让它重新走到开头,如图:
当然,当我们插入完数据后,不要忘记返回true,如下:
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
if(myCircularQueueIsFull(obj))
{
return false;
}
obj->arr[obj->rear++] = value;
obj->rear %= obj->capacity + 1;
return true;
}
出队列
在出队列之前,我们首先要判断循环队列能否出队列,如果它为空,说明我们不能再删除数据了,就要直接返回false
判断完之后我们就要真正来实现出队列操作了,出队列就是删除数组头的数据,但是由于我们不需要释放空间,并且这是数组,所以我们可以直接让front++,间接实现头删,如图:
但是同样有一个问题,那就是front++后也可能越界,比如上面的例子中,如果再入队列几次,再去出队列,front++,是不是就刚好越界了,所以front也是有可能越界的
为了防止越界,让循环队列首尾相连,也只需要让front % (capacity + 1)即可,跟上面入队列是rear的解决方式一样,这里就不多说了,有了思路之后我们直接来写代码,如下:
bool myCircularQueueDeQueue(MyCircularQueue* obj)
{
if(myCircularQueueIsEmpty(obj))
{
return false;
}
obj->front++;
obj->front %= obj->capacity + 1;
return true;
}
5.循环队列取队头和队尾元素
在实现取队头和队尾元素操作之前,我们来看看题目的要求:
题目提示我们,如果循环队列为空是不是就不能取到队头和队尾元素了,所以如果我们判断出来循环队列为空,需要直接返回-1
取队头元素
在取队头元素之前我们需要判断循环队列是否为空,为空就返回-1,不为空我们才去取队头元素,方法也很简单,就是直接返回front位置的数据即可,如下:
int myCircularQueueFront(MyCircularQueue* obj)
{
if(myCircularQueueIsEmpty(obj))
{
return -1;
}
return obj->arr[obj->front];
}
取队尾元素
在取队尾元素之前我们需要判断循环队列是否为空,为空就返回-1,不为空我们才去取队尾元素,但是取队尾元素会麻烦一点,因为rear不是指向最后一个有效数据,而是指向有效数据的下一个位置
那么是不是直接返回rear-1位置的数据就可以了呢?大部分情况下是这样的,但是有一种例外,就是当rear = 0时,如果直接-1的话就变成了-1了,越界了,而它的前一个位置应该是原队列中最后一个元素,如图:
所以我们可以用重新定义一个prev,让它等于rear-1,如果prev < 0,我们就让它加上循环队列的真实大小(capacity+1),在上图中rear-1为-1,加上真实大小6就变成了5,成功回到了最后一个位置
处理完prev可能小于0的情况后,就可以直接返回数组中prev位置的数据的,如下:
int myCircularQueueRear(MyCircularQueue* obj)
{
if(myCircularQueueIsEmpty(obj))
{
return -1;
}
int prev = obj->rear-1;
if(prev < 0)
{
prev += obj->capacity + 1;
}
return obj->arr[prev];
}
6.循环队列的销毁
循环队列的销毁是最简单的,只要循环队列底层的数组不为空,就将它的空间释放掉,最后释放掉整个循环队列,如下:
void myCircularQueueFree(MyCircularQueue* obj)
{
if(obj->arr)
free(obj->arr);
free(obj);
obj = NULL;
}
7.最后题解源码
typedef struct
{
int* arr;
int capacity;
int rear;
int front;
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k)
{
MyCircularQueue* pq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
if(pq == NULL)
{
perror("malloc");
return NULL;
}
pq->arr = (int*)malloc(sizeof(int) * (k+1));
if(pq->arr == NULL)
{
perror("malloc");
return NULL;
}
pq->rear = pq->front = 0;
pq->capacity = k;
return pq;
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj)
{
return obj->front == obj->rear;
}
bool myCircularQueueIsFull(MyCircularQueue* obj)
{
return (obj->rear + 1) % (obj->capacity + 1) == obj->front;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
if(myCircularQueueIsFull(obj))
{
return false;
}
obj->arr[obj->rear++] = value;
obj->rear %= obj->capacity + 1;
return true;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj)
{
if(myCircularQueueIsEmpty(obj))
{
return false;
}
obj->front++;
obj->front %= obj->capacity + 1;
return true;
}
int myCircularQueueFront(MyCircularQueue* obj)
{
if(myCircularQueueIsEmpty(obj))
{
return -1;
}
return obj->arr[obj->front];
}
int myCircularQueueRear(MyCircularQueue* obj)
{
if(myCircularQueueIsEmpty(obj))
{
return -1;
}
int prev = obj->rear-1;
if(prev < 0)
{
prev += obj->capacity + 1;
}
return obj->arr[prev];
}
void myCircularQueueFree(MyCircularQueue* obj)
{
if(obj->arr)
free(obj->arr);
free(obj);
obj = NULL;
}
那么今天的刷题就分享到这里,整个栈和队列的题就暂时分享到这里,有什么疑问欢迎提出,后面我们就进入二叉树的学习啦,敬请期待吧!
bye~