栈和队列oj详解(下)

简介: 栈和队列oj详解

四、设计循环队列


来源:leetcode:622、设计循环队列


1、题目描述


设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。


循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。


你的实现应该支持如下操作:


MyCircularQueue(k): 构造器,设置队列长度为 k 。

Front: 从队首获取元素。如果队列为空,返回 -1 。

Rear: 获取队尾元素。如果队列为空,返回 -1 。

enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。

deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。

isEmpty(): 检查循环队列是否为空。

isFull(): 检查循环队列是否已满。


2、题目分析


循环队列,本质还是实现一个队列,实现这个队列的基本几个接口,只不过这个队列是有限长度的,并且这个队列是循环的。


1669251402032.jpg


这个循环在我们的理解大概是个环状,这里要怎么实现呢?用数组还是链表来实现呢?乍一看,这个循环队列和我们循环链表的概念很相似,是否链表就优于数组呢?别着急,我们再来细品一下:


我们先图解看一下使用链表和数组的图解:


1669251411973.jpg



结合这个图解,博主把每个接口需要的功能先大致分析一下:


1、Front(队首) 和Rear(队尾)这两个接口比较简单,就是直接使用两个指针指向队首和队尾,然后返回就可以了。队首两种方式都可以,队尾的话如果是数组,直接找下标就能找到队尾的下标然后返回,(不过这一点还有一点细节需要注意,如果没发现还是会出错,博主后面还会说)但是链表,因为链表的尾指针指向的位置是没有填数据的,也就是队尾的next,所以需要遍历找到尾指针的上一个位置,比较麻烦。


2、enQueue(value): 向循环队列插入一个元素,这个函数也比较简单,就是尾插,我们设有一个尾指针,插入然后++就可以了。数组唯一需要注意的就是


3、deQueue(value):从循环队列删除一个元素,队列先入先出的功能,就是头删,头指针++。


4、isEmpty(): 检查循环队列是否为空。isFull():判断循环队列是否满了,这两个问题比较有趣


1669251421227.jpg


分析一下我们就会发现,判空和判满无法区分,怎么解决呢?


我们创造性的提出了一种方式,类似于链表中增加一个哨兵位,我们多加一个位置,但是不填数据。比如:


1669251437102.jpg


我们发现,当我们加了一个位置的时候,这时候为空和为满的情况是不相同的,这时候是可以区分开两种情况的,说明增加一个位置确实是可行的,而且这个位置是不断变化的。


上面的图是数组的情况,这里需要处理的细节就是back如何和front建立联系,以及当back在尾的时候怎么回到头,这一点也和博主上面说的插入的数组需要考虑的情况相符合。这些都是数组需要考虑的。我们再来设想链表的话,似乎就不需要考虑这些问题,只需要back的next指针是否为front即可。


3、代码编译


博主用数组和链表都实现了一遍,都可以,各有利弊吧。


①数组的方式:

//数组去写
typedef struct 
{
    int*a;
    int front;
    int back;
    int capacity;
} MyCircularQueue;
bool myCircularQueueIsFull(MyCircularQueue* obj);
bool myCircularQueueIsEmpty(MyCircularQueue* obj);
MyCircularQueue* myCircularQueueCreate(int k) 
{
    MyCircularQueue* obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    int* tmp=(int*)malloc(sizeof(int)*(k+1));
    if(tmp==NULL)
    {
        perror("malloc fail");
        return NULL;
    }
    else
    {
        obj->a=tmp;
        obj->front=obj->back=0;
        obj->capacity=k+1;
    }
    return obj;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) 
{
    //插入元素
    if(!myCircularQueueIsFull(obj))
    {
        //如果没满
        obj->a[obj->back]=value;
        obj->back++;
        obj->back%=obj->capacity;//主要是要考虑到到了最后一个位置
        return true;
    }
    else
    {
        return false;
    }
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) 
{
    if(!myCircularQueueIsEmpty(obj))
    {
        //没空才能删
        obj->a[obj->front]=0;//虽然没啥意义
        obj->front++;
        obj->front%=obj->capacity;
        return true;
    }
    else
    {
        return false;
    }
}
int myCircularQueueFront(MyCircularQueue* obj) 
{
    if(!myCircularQueueIsEmpty(obj))
        return obj->a[obj->front];
    else
        return -1;
}
int myCircularQueueRear(MyCircularQueue* obj) 
{
     if(!myCircularQueueIsEmpty(obj))
        return obj->a[(obj->back-1+obj->capacity)%obj->capacity];//要考虑到back在第一个的情况
    else
        return -1;
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) 
{
    return obj->front==obj->back;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) 
{
    return (obj->back+1)%obj->capacity==obj->front;
}
void myCircularQueueFree(MyCircularQueue* obj) 
{
    free(obj->a);
    obj->front=obj->back=0;
    obj->capacity=0;
    free(obj);
    obj=NULL;
}


理解了上面的图解,用数组的方式来实现代码还是比较简单的,不过需要注意颇多的细节,


博主认为这些地方都是不易想到,需要注意的,而且可以说是点睛之笔。


1、第一个%主要是,当最后一个位置被填之后,如何回到队首,这个%可以说十分巧妙,直接回到队首。


1669251483155.jpg

1669251492216.jpg


2、第二个front也是同理,删到队尾的时候怎么回到队首。


1669251505734.jpg

1669251514036.jpg


3、而第三个找队尾元素,这样设计的原因是要考虑到,如果back在队首,那么队尾肯定是最后一个位置的元素,怎么找到呢?这种方式可以说很好的解决了这个问题。


1669251528996.jpg

1669251537586.jpg


4、而第四个则是为了解决判满的问题,当满的时候。back的下一个位置就是front,这样设计是为了解决back在最后一个位置,而front在第一个位置的情况。


1669251550619.jpg

1669251569816.jpg


这样操作不仅解决特殊情况,而且具有普适性,十分实用,但也不易被想到。


②链表的方式

typedef struct MCQueue
{
    int data;
    struct MCQueue* next;
} MCQueue;
typedef struct
{
    MCQueue* front;
    MCQueue* back;
}MyCircularQueue;
bool myCircularQueueIsEmpty(MyCircularQueue* obj);
bool myCircularQueueIsFull(MyCircularQueue* obj);
MCQueue* BuyNewnode()
{
     MCQueue* newnode=(MCQueue*)malloc(sizeof(MCQueue));
    if(newnode==NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    else
    {
        newnode->next=NULL;
    }
    return newnode;
}
MyCircularQueue* myCircularQueueCreate(int k) 
{
    //初始化
    MCQueue* CQueue=NULL;
     MCQueue* tail=NULL;
    int N=k+1;//多加一个位置
    //构建链表
    while(N--)
    {
         MCQueue* newnode=BuyNewnode();
         if(CQueue==NULL)
         {
             CQueue=newnode;
             //方便找尾
             tail=CQueue;
         }
         else
         {
             //头插的一种方式
             newnode->next=CQueue;
             CQueue=newnode;
         }
         tail->next=CQueue;
         //链接起来
    }
    MyCircularQueue* obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    obj->front=CQueue;
    obj->back=CQueue;
    return obj;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) 
{
    if(!myCircularQueueIsFull(obj))
    {
        obj->back->data=value;
        //指向下一个
        obj->back=obj->back->next;
        return true;
    }
    else
        return false;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) 
{
    //删的前提是没空
    if(!myCircularQueueIsEmpty(obj))
    {
        obj->front=obj->front->next;
        return true;
    }
    else
        return false;
}
int myCircularQueueFront(MyCircularQueue* obj) 
{
    if(!myCircularQueueIsEmpty(obj))
    {
        return obj->front->data;
    }
    else
    {
        return -1;
    }
}
int myCircularQueueRear(MyCircularQueue* obj) 
{
    //这个比较麻烦,需要找到back的前一个
    if(!myCircularQueueIsEmpty(obj))
    {
        MCQueue* cur=obj->front;
        while(cur->next!=obj->back)
        {
            cur=cur->next;
        }
        //找到前一个
        return cur->data;
    }
    else
        return -1;
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) 
{
    return obj->front==obj->back;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) 
{
    //如何判满?
    return obj->back->next==obj->front;
}
void myCircularQueueFree(MyCircularQueue* obj) 
{
    MCQueue* cur=obj->front->next;
    while(cur!=obj->front)
    {
        MCQueue* next=cur->next;
        free(cur);
        cur=next;
    }
    free(cur);
    free(obj);
}

从逻辑上来讲,链表更符合循环链表的逻辑,不过用链表比较怪怪的,但是构建就比较麻烦,需要一个一个链表去开创,还要去链接起来,更古怪的是这些链表什么数据也不放,不太合习惯,而且用链表来实现比较恶心的有两点:


1、链表的初始化和链接,以及开创之后,因为这是一个环形链表,而且没有下标,那么哪个是头,哪个是尾需要你自己去定义。


2、在找尾的时候,需要再次遍历一下,时间复杂度是0(N)。


优点就是:


不需要去特殊处理数组需要处理的那些情况,初始化之后写起来很顺。


好了,本期的oj分享就到这里了,希望大家可以养成良好的编程习惯,多画图分析,争取一次过,而不是修修补补。

相关文章
|
1月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
32 1
|
25天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
48 5
|
1月前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
1月前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
1月前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
49 0
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
196 9
|
1月前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
51 4
|
2月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
48 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
1月前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
2月前
初步认识栈和队列
初步认识栈和队列
65 10