数据结构刷题训练:设计循环队列(力扣OJ)

前言

        当谈到队列数据结构时,很多人可能会想到普通的队列,即先进先出(FIFO)的数据结构。然而,有时候我们需要一种更高效的队列实现方式,这就是循环队列。


1. 题目:设计循环队列

 题目描述:

 题目链接:

设计循环队列icon-default.png?t=N6B9https://leetcode.cn/problems/design-circular-queue/

2. 思路

        先来理解一下题意,首先队列的长度为定长k,其次就是队列可以循环利用空间。这里我们要思考一下是使用链表实现,还是使用数组实现。在设计这个环形队列的时候,我们肯定是要设计头和尾的入下图:

         当前状态视为队列满,这时如果出队元素就会空出空间此时我们可以继续入队数据如下图:

         后边满了以后再插入数据它可以绕回来,这就是循环队列。从front开始道rear结束就是队列的数据。或许有人会有疑惑:为什么不把尾指向最后一个元素,而是指向最后一个元素的后一个位置?

        这样是为了便于判断队列是否为空,我们举个例子,队列为空时front和rear是都指向开头的,如果插入一个数据,rear就要向后移动,不移动就无法判断队列是否为空。

接下来我们回到开始的问题,使用数组还是链表实现?

         我们依据上一个问题,知道rear指向的是队尾的后一个元素,那依据单链表的特性取队尾不好取。如果要好取尾就要用双向循环链表会好搞一些,当然还有其他的解决办法例如:多加一个变量size记录队列数据个数,rear指向队尾,这样也可以解决,但这样写代码会很容易让人误导,所以这里不推荐这样的方法。链表实现也是可以的,但相对数组的代码量就比较多了。使用数组实现也会简单方便的多。

 数组:

         当rear+1==front时就是为满,由于它是一个循环队列,rear在末尾后再+1就会回到头,这里可以写成这样:front==(rear+1)%(k+1)(取值范围0~k),它要始终保持front与rear之间留有一个空。这里我们就使用数组来实现。

3. 分析

        依据上述的思路,我们使用数组来实现循环队列,通过上述的思路我们可以发现这个规律,出队就front向后走,入队就rear向后走。

 3.1 定义循环队列

typedef struct {
    int* a;
    int front;
    int rear;
    int k;
} MyCircularQueue;

        a用于存放数据,和顺序表类似,使用malloc开辟空间。front为队头,rear为队尾。

 3.2 创建队列

MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));

    //多开一个空间
    obj->a=(int*)malloc(sizeof(int)*(k+1));
    obj->front=obj->rear=0;
    obj->k=k;

    return obj;
}

         这里和之前一样,我们仅仅是定义了队列的类型,并没有创建结构体变量,为了便于后续传参,这里我们使用malloc创建一个结构体变量。有了obj就可以通过它来找到结构体中的各个成员。注意我们还需要对数组a进行开辟空间。

 3.3 判空和判满

         这里我们先来实现判满和判空操作,上述思路中我们多开辟一个空间,用于判断满和空的情况,如果rear等于front就为空,如果rear+1等于front就为满。接下来我们来实现一下:

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return (obj->front==obj->rear);
}

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return (obj->front==(obj->rear+1));
}

         但这样写对吗?这样写是不对的,前边思路中提到:front==(rear+1)%(k+1)为了达到返回开头的目的,所以这里需要改写(也是为了防止rear+1越界)。

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return (obj->front==obj->rear);
}

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return (obj->front==(obj->rear+1)%(obj->k+1));
}

        这里可能就有人疑惑了,为什么rear需要模k+1返回,front就不需要呢?front也会走到尾啊。

这个当然是需要考虑的,这个操作是在出队操作时考虑的,在判满时可以不考虑,满的情况分为两种:

第一种:

 第二种:

 3.4 入队

 

bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    if(myCircularQueueIsFull(obj))
        return false;

    obj->a[obj->rear]=value;
    obj->rear++;

    obj->rear%=(obj->k+1);
    return true;
}

         入队操作非常简单,在入队之前先判断队是否满,如果满直接返回false(错误)。不为满就将rear位置入队数据,然后rear向后移动,这里为了便于rear返回,这里rear需要%=(k+1)。

过程如下:

 

 3.5 出队

 

bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
        return false;

    obj->front++;
    obj->front %=(obj->k+1);
    return true;
}

         出队前要先判断队列是否为空,不为空就front++即可不需要任何修改(front开始到rear直接的为有效数据),为了便于front返回,这里可以使用取模,或者使用if语句判断,都可以。

 

 再次入队数据就会将原数据自动覆盖。

 3.6 取队头队尾数据

         取队头数据非常简单

int myCircularQueueFront(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
        return -1;
    else
        return obj->a[obj->front];
}

         如果为空就直接返回-1,如果不为空就返回数组front下标的数据。重点在于队尾数据。

         一般情况队尾数据只需要rear-1就可以了,但是这里需要考虑rear为0的情况。如果rear为0,rear-1就越界非法访问了,我们需要的是rear返回到数组最后的位置。这里我们可以这样操作,既然我们是取模让rear达到返回到0,那我们在对rear取模之前先对rear+(k+1),这样rear取模后仍然可以取到下标为0的位置,-1就回到了最后的位置(减一后:(rear+k)%(k+1)结果为k)

 (rear+(k+1)-1)%k+1化简一下就是:(rear+k)%(k+1),代码实现:

int myCircularQueueRear(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
        return -1;
    else
        return obj->a[(obj->rear+obj->k)%(obj->k+1)];
}

 3.7 销毁队列

        销毁队列也非常简单,先销毁啊数组,再销毁obj。

void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->a);
    free(obj);
}

 4. 题解

 整体代码:

typedef struct {
    int* a;
    int front;
    int rear;
    int k;
} MyCircularQueue;


MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    obj->a=(int*)malloc(sizeof(int)*(k+1));
    obj->front=obj->rear=0;
    obj->k=k;
    return obj;
}

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return (obj->front==obj->rear);
}

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return (obj->front==(obj->rear+1)%(obj->k+1));
}

bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    if(myCircularQueueIsFull(obj))
        return false;

    obj->a[obj->rear]=value;
    obj->rear++;

    obj->rear%=(obj->k+1);
    return true;
}

bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
        return false;

    obj->front++;
    obj->front %=(obj->k+1);
    return true;
}

int myCircularQueueFront(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
        return -1;
    else
        return obj->a[obj->front];
}

int myCircularQueueRear(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
        return -1;
    else
        return obj->a[(obj->rear+obj->k)%(obj->k+1)];
}



void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->a);
    free(obj);
}

 力扣上边也是可以通过的。

 

         通过整体代码实现我们可以发现:数组实现循环队列需要特别注意边界问题,一不注意就会出现错误。


 

总结

        希望本博客能够帮助您更好地理解和应用循环队列,为您的学习和工作带来帮助。让我们继续探索数据结构的奥秘,不断提升自己的编程能力吧!

  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 31
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值