【初阶数据结构和算法】leetcode刷题之设计循环队列

文章目录

一、实现循环队列

题目链接: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++即可,到后面出队列再细讲,那么最后循环队列的大致结构定义如下:

c 复制代码
typedef struct 
{
    int* arr;
    int capacity;
    int rear;
    int front;
} MyCircularQueue;

初始化

在初始化函数中给了我们一个参数k,就是我们要申请的空间大小,我们直接通过动态申请k个整型(后面可能会需要更改,我们先实现到这里),然后将容量置为k,两个指向头和尾的下标置为0,如下:

c 复制代码
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个空间,其它不变,如下:

c 复制代码
    pq->arr = (int*)malloc(sizeof(int) * (k+1));

这个做完之后我们就可以直接来实现判空和判满了

判空

判空的条件就是循环队列的front等于rear,如下:

c 复制代码
bool myCircularQueueIsEmpty(MyCircularQueue* obj) 
{
    return obj->front == obj->rear;
}

判满

判满的条件就是循环队列 (rear + 1) % (capacity + 1) == front,如下:

c 复制代码
bool myCircularQueueIsFull(MyCircularQueue* obj) 
{
    return (obj->rear + 1) % (obj->capacity + 1) == obj->front;
}

4.循环队列的入队列和出队列

我们先来看看题目给出入队列和出队列操作的要求,如图:

题目的意思是如果我们插入或删除成功需要返回true,虽然题目没有说,但是我们应该都能想到插入和失败是不是就要返回false了,所以我们在入队列和出队列之前要记得判断能否插入和删除数据,那么接下来我们进入对队列和出队列的具体分析

入队列

在入队列之前,我们首先要判断循环队列能否入队列,如果它满了,说明我们不能再插入数据了,就要直接返回false

判断完之后我们就要真正来实现入队列操作了,入队列就是往数组的最后插入数据,也就是往rear的位置插入数据,插入完之后让rear++

但是我们要注意一个点,rear++之后可能会越界,我们要让它模上数组真正大小,也就是capacity+1,让它重新走到开头,如图:

当然,当我们插入完数据后,不要忘记返回true,如下:

c 复制代码
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的解决方式一样,这里就不多说了,有了思路之后我们直接来写代码,如下:

c 复制代码
bool myCircularQueueDeQueue(MyCircularQueue* obj) 
{
    if(myCircularQueueIsEmpty(obj))
    {
        return false;
    }
    obj->front++;
    obj->front %= obj->capacity + 1;
    return true;
}

5.循环队列取队头和队尾元素

在实现取队头和队尾元素操作之前,我们来看看题目的要求:

题目提示我们,如果循环队列为空是不是就不能取到队头和队尾元素了,所以如果我们判断出来循环队列为空,需要直接返回-1

取队头元素

在取队头元素之前我们需要判断循环队列是否为空,为空就返回-1,不为空我们才去取队头元素,方法也很简单,就是直接返回front位置的数据即可,如下:

c 复制代码
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位置的数据的,如下:

c 复制代码
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.循环队列的销毁

循环队列的销毁是最简单的,只要循环队列底层的数组不为空,就将它的空间释放掉,最后释放掉整个循环队列,如下:

c 复制代码
void myCircularQueueFree(MyCircularQueue* obj) 
{
    if(obj->arr)
        free(obj->arr);
    free(obj);
    obj = NULL;
}

7.最后题解源码

c 复制代码
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~

相关推荐
潜洋2 分钟前
Spring Boot教程之七: Spring Boot –注释
java·spring boot·后端·注释
小丁爱养花9 分钟前
网络原理(一):应用层自定义协议的信息组织格式 & 初始 HTTP
java·服务器·网络·网络协议·http
天天要nx11 分钟前
D79【 python 接口自动化学习】- python基础之HTTP
python
icesmoke18 分钟前
Codeforces Round 987 (Div. 2)(A-D)
数据结构·c++·算法
ystraw_ah21 分钟前
python 画图例子
python
mljy.21 分钟前
C++《二叉搜索树》
c++·学习
程序猿进阶23 分钟前
Otter 安装流程
java·数据库·后端·mysql·数据同步·db·otter
oioihoii26 分钟前
建造者模式
c++·设计模式·c#·建造者模式
暗夜绿28 分钟前
【GoF23种设计模式】01_建造者模式
java·设计模式·建造者模式