当前位置: 首页 > article >正文

【初阶数据结构和算法】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~


http://www.kler.cn/a/409682.html

相关文章:

  • JavaScript 中通过Array.sort() 实现多字段排序、排序稳定性、随机排序洗牌算法、优化排序性能,JS中排序算法的使用详解(附实际应用代码)
  • Python办公自动化案例:将演示文稿批量导出为图片和PDF文件
  • 前端速通(CSS)
  • 标贝科技大模型声音复刻 快速获取高品质专属AI声音
  • 【Rhino】【Python】Create a series of Blocks according to Value of object Property
  • 【力扣算法题】双指针-战场上的矛与盾的组合(移动零)(快乐数)
  • QRCode.toDataURL() vue3 uniapp h5在 Android环境下二维码显示不出来
  • JVM(六、Java内存分配)
  • python+pytest+allure利用fix实现接口关联
  • AI 驱动的个性化推荐系统设计
  • Spring Boot英语教育网站:从零到一
  • 学习与理解LabVIEW中多列列表框项名和项首字符串属性
  • 什么是Three.js,有什么特点
  • 怎么建设一套电话机器人系统?
  • ES 和Kibana-v2 带用户登录验证
  • CPU性能优化--采集调用栈
  • HarmonyOS:@State、@Prop、@Link
  • HandyControl简单应用
  • 电脑桌面备忘录提醒,备忘录提醒工具设置
  • 微服务系统架构图
  • 健身房小程序服务渠道开展
  • 如何用通义灵码快速绘制流程图?
  • 网络爬虫——分布式爬虫架构
  • 解读 Keep-Alive:CSDN 项目实例分析
  • Spring Boot框架:英语知识网站构建指南
  • 麒麟系统状态监控