经典OJ——队列实现栈 栈实现队列 循环队列的实现 C语言实现

目录

        一、队列实现栈

        二、栈实现队列

        三、循环队列的实现


       一、队列实现栈

        队列的特性是先进先出,栈的特性是后进先出,要实现这两种特性的转化,我们就需要一个额外的队列来进行倒数据的操作,需要用到的队列的代码可以参考我之前的博客队列的实现

        由队列实现栈首先要先构建出这个栈的结构体,经过前面的分析,我们可以知道这个栈的结构体成员是两个队列。这里用到的是匿名结构体,直接定义了MyStack这个结构体。

typedef struct {
    Queue q1;
    Queue q2;
} MyStack;

        和平常的初始化栈不同的是,这里在初始化时,用到的函数是一个无参的,返回时这个结构体指针类型的函数;所以我们这个栈所需要的空间需要我们在这个函数内开辟,开辟好以后只需要用已经写好的队列的初始化函数对这个栈内的两个队列分别初始化即可。

MyStack* myStackCreate() {
    MyStack* st=(MyStack*)malloc(sizeof(MyStack));
    QueueInit(&st->q1);
    QueueInit(&st->q2);
    return st;
}

        因为这个栈内有两个队列,其中一个队列是用来倒数据的,一个是用来存数据的,也就是说,为了满足栈后进先出的特性,所以我们要“固定”的往两个队列中,空的那个队列中插入数据。

void myStackPush(MyStack* obj, int x) {
    if(!QueueEmpty(&obj->q1))
    {
        QueuePush(&obj->q1,x);
    }
    else
    {
        QueuePush(&obj->q2,x);
    }
}

        出栈的时候就需要用到另外一个非空的队列的,因为在构造栈的时候就多构建了一个队列用来倒数据,所以,就可以把有数据的那队列中的内容先往另外一个队列中倒,直到先前存有数据的那个队列中只剩下一个数据为止,这个剩下的数据就是最后入栈的数据,同时因为队列的特性,在另外一个队列中,数据的存放顺序是没有改变的,只要把那个最后的数据弹出队列,就可以实现栈后进先出的特点,同时又把两个队列分为了一个存数据的队列,一个准备用来倒数据的空队列。

        这里使用了假设法,先假设第一个队列是空的,第二份队列为非空队列,然后在判断第一个队列是否为空,如果第一个队列不为空,则第二个队列为空(由题目可知每次调用 pop 和 top 都保证栈不为空),这样就只需要对这个空队列和非空队列进行操作即可,不用分情况讨论。

        当要把数据出栈时,就要把非空队列中的数据倒入空队列中,直到非空队列中的数据只剩一个,这个数据就是我们要返回的值,用一个临时变量把这个数据保存下来,再把这个数据从非空队列中弹出,即可实现Pop操作。

int myStackPop(MyStack* obj) {
    Queue* empty=&obj->q1;
    Queue* nonEmpty=&obj->q2;
    if(!QueueEmpty(&obj->q1))
    {
        empty=&obj->q2;
        nonEmpty=&obj->q1;
    }

    while(QueueSize(nonEmpty)>1)
    {
        QueuePush(empty,QueueFront(nonEmpty));
        QueuePop(nonEmpty);
    }
    int ret=QueueFront(nonEmpty);
    QueuePop(nonEmpty);
    return ret;
}

        至于取栈顶元素,取得也就是非空的队列中的队尾数据。

int myStackTop(MyStack* obj) {
    if(!QueueEmpty(&obj->q1))
        return QueueBack(&obj->q1);
    else
        return QueueBack(&obj->q2);
}

        这个栈要为空要满足的条件为这两个队列同时为空,则这个栈中没有数据。

bool myStackEmpty(MyStack* obj) {
    return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}

        在销毁时也要先销毁在这个栈内部的队列动态开辟的空间以后,才能销毁这个栈的空间,如果先销毁栈的空间,就会找不到内部那两个队列申请的空间,造成内存泄漏问题。

void myStackFree(MyStack* obj) {
    QueueDestory(&obj->q1);
    QueueDestory(&obj->q2);
    free(obj);
}

        二、栈实现队列

        用栈实现队列的思路和队列实现栈的思路类型,也是在这个队列的内部需要两个栈,同时一个用来存数据,一个用来倒数据;与队列实现栈不同的是,队列内部的两个栈是固定的,一个用来存数据,一个用来取数据。这里用到的栈操作相关的代码可以参考我的博客动态栈的实现

        因为队列内部栈的功能是固定的,所以在命名的时候就根据功能来区分,方便后续操作。

        当存数据的栈内有数据时,如果直接在这个栈上弹出数据,此时会把我们最后放入的数据弹出,要实现队列先进先出的顺序,我们就可以借助另外一个栈来实现,和队列实现栈一样,把存数据的栈内所有的数据倒入出数据的栈内,如下图,这两种方式都可以实现出队的功能,这里我们用上面一种方式实现。

        这里与队列实现栈不同的地方就出现了,如果我们继续往不是空的那个栈内入数据的话,就会破坏队列中数据的顺序,出队的时候就会出现问题,如图,如果按照队列实现栈一样的方式,本该轮到“2”出队,却变成了5。

        但是转化一下思路,在第一个图中,出数据的栈把数据1出队以后下一个数据就是数据2,这个数据2不就正好是队头数据,也就是我们下一个要出队的数据吗,所以只要把入队和出队所使用的栈固定了,就可以利用栈后进先出的特性实现队列先进先出的特性。

typedef struct MyQueue{
    ST inst;
    ST outst;
} MyQueue;

        这里的初始化和前面队列实现栈的初始化一样,需要先动态开辟队列所在的空间,再对内部的两个栈进行初始化即可。

MyQueue* myQueueCreate() {
    MyQueue* que = (MyQueue*)malloc(sizeof(MyQueue));
    STInit(&que->inst);
    STInit(&que->outst);

    return que;
}

        入队不需要复杂的处理,只要把数据放入存数据的栈即可。

void myQueuePush(MyQueue* obj, int x) {
    STPush(&obj->inst, x);
}

        出队是就要考虑复杂的情况了,当出数据的栈内有数据的时候,只要把栈顶数据出栈即可;当出数据的栈内没有数据的时候,我们就要先把入数据的栈内所有的数据都先依次导入出数据的栈内,再弹出栈顶元素即可。

int myQueuePop(MyQueue* obj) {
    if (STEmpty(&obj->outst))
    {
        while (!STEmpty(&obj->inst))
        {
            STPush(&obj->outst, STTop(&obj->inst));
            STPop(&obj->inst);
        }
    }

    int top = STTop(&obj->outst);
    STPop(&obj->outst);
    return top;
}

    要取出队头数据时,因为栈的操作只能取得栈顶的数据,所以当出数据的栈内有数据时,栈顶元素就是我们要取的数据,当出数据的栈内没有数据时,我们要取的元素被“压”在了入数据的栈底位置,因此我们要先把数据倒入到出数据的栈内,再取出栈顶元素。    

int myQueuePeek(MyQueue* obj) {
    if (STEmpty(&obj->outst))
    {
        while (!STEmpty(&obj->inst))
        {
            STPush(&obj->outst, STTop(&obj->inst));
            STPop(&obj->inst);
        }
    }

    int top = STTop(&obj->outst);
    return top;
}

        同样队列内部的两个栈都为空的时候,这个队列才为空。

bool myQueueEmpty(MyQueue* obj) {
    return STEmpty(&obj->inst) && STEmpty(&obj->outst);
}

        在释放内存空间的操作也是类似的,要先释放队列内部的两个栈的空间再释放这个队列的空间。

void myQueueFree(MyQueue* obj) {
    STDestory(&obj->inst);
    STDestory(&obj->outst);

    free(obj);
}

        三、循环队列的实现

        循环队列是让我们构建一个长度一定,但是首尾相连的一个队列,它同样满足队列的先进先出原则,同时在队列满的时候,就无法插入新的数据。

        这里和普通的队列不相同的是,是一个长度固定并且首尾相连这个特性,所以它才能循环起来,我们可以先从这个这个队列满的状态和空的状态开始分析。这个循环队列既可以用链表实现也可以用数组实现,其中大同小异,这里拿用数组实现作为样例。

        这里我们假设这个循环队列的长度是4,我们要在队头位置进行入队操作,在队尾进行出队操作,因此我们需要有两个参数来记录这两个位置,因为长度队列的长度是4,我们就给队列申请4空间,所以队列的初始状态为:head和tail相等,此时head表示队头的位置,tail表示的是队尾的下一个位置。

        当我们不断向这个队列中入数据时,我们会发现,当队列满时,表示队头的head和表示队尾的tail又相等了,此时的状态和队列为空的状态一样了,变得没办法区分开了,这里的第一种方式是给这个循环队列增加一个参数size表示队列内的数据,由这个来区分到底是空还是满的状态,但是有一种更巧妙的方式,后面我们都用第二种方式来实习。

        第二种方式就是多申请一块空间,也就是说,假设我们的循环队列大小是4,我们就申请一块大小为5的数组的空间来作为循环队列,这样就可以解决“假空”的问题了。

        同样是一个大小为4的循环队列,初始时,它的状态为空,此时head与tail相等。

        当我们不断向循环队列中插入数据,这个时候我们可以发现,当循环队列满时,此时的head和tail的位置不相同了,tail的下一个位置才时head,这样就把空和满的两种状态区分开了。

        由上分析可以知道,我们在构造这个循环队列的时候,首先是需要一个存放数据的数组,然后还有由两个成员,分别指向对头的位置和队尾的位置,最后还要有一个成员变量记录这个数据到底能存放几个数据。

typedef struct MyCircularQueue {
    int* arr;
    int head;
    int tail;
    int k;
} MyCircularQueue;

        创造循环队列的时候需要注意的就是要多开辟一个存放数据的空间,用来解决“假空”的问题。

MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* q = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    q->arr = (int*)malloc(sizeof(int) * (k + 1));
    q->head = 0;
    q->tail = 0;
    q->k = k;
    return q;
}

        因为tail表示的是队尾的下一个位置,如果队尾的下一个位置和队头相等,说明这队列中间并没有数据,也就可以用来判空了。

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    if (obj->head == obj->tail)
        return true;
    else
        return false;
}

        从上面的分析可以得出如果tail的下一个位置和head相同的话,那就是队列满的状态,这里要考虑到的情况是,tail的下一个位置就是tail+1,当tail+1不在这个数组之内,因为数组的长度为k+1,所以数组的最后一个位置的下标就是k也就是说数组的下标是0~k,所以我们只要把tail+1的值都模上一个k+1就可以避免出现tail+1出现在数组之外的情况,当tail+1小于k+1时,对结果并没有影响。

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    if ((obj->tail + 1) % (obj->k + 1) == obj->head)
        return true;
    else
        return false;
}

       入队操作:当队列满时,不在插入数据,此时返回false;当队列不为空时,因为tail表示的就是队尾数据的下一个位置,所以直接把数据插入到tail位置出即可,然后再把tail+1,同样要进行防止tail溢出的操作。

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

    obj->arr[obj->tail] = value;
    obj->tail = (obj->tail + 1) % (obj->k + 1);
    return true;
}

        出队操作:当队列为空时,出队失败,返回false;当队列不为空的时候,只要把head+1,我们就取不到这个数了,相当于这个数据就已经出队了,同样的也要队head进行防止溢出的操作。

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

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

        取队头数据:依题意,当队列为空时,返回-1;当队列不为空是,head位置就是队头位置,所以返回head位置的数据即可。

int myCircularQueueFront(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj))
    {
        return -1;
    }
    return obj->arr[obj->head];
}

        取队尾数据:依题意,当队列为空时,返回-1;当队列不为空时,tail指向的是队尾的下一个位置,所以我们要的是tail-1位置的数据,但会出现下图的情况,tail-1的位置变成了-1,此时就数组就越界访问,所以我们就要对tail进行处理,因为数组的下标为0~k,所以我们给tail-1模一个k+1并不会改变tail-1在数组中表示的位置,但是这还没有解决tail-1可能变为负数的情况,因为我们在后面模了一个k+1,那在tail-1的位置加上一个k+1也是不会改变tail-1在数组中的位置的,例如tail=1,那么(tail-1+k+1)%(k+1)还是1,当tail=0时,(tail-1+k+1)%(k+1)就变成了k,这样就解决了tail-1可能变为负数的情况。

int myCircularQueueRear(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj))
    {
        return -1;
    }
    return obj->arr[(obj->tail + obj->k) % (obj->k + 1)];//原型为 (obj->tail-1 + k +1) % (k+1)
}

        销毁循环队列:同样也要先销毁队列内部存放数据的空间,再把指向这块空间的指针指空,最后再释放循环队列的空间即可。

void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->arr);
    obj->arr = NULL;
    free(obj);
}

  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值