【经典数据结构OJ讲解】你知道如何用两个队列实现一个栈,如何用两个栈实现一个队列吗?

本文详细介绍了如何使用两个队列来实现一个栈,以及如何用两个栈实现一个队列。在C语言中,通过初始化、销毁、插入、删除、获取栈顶元素和判断栈空等接口,实现了仿生栈和队列。同时,文章提供了代码实现思路和具体接口的详细步骤。
摘要由CSDN通过智能技术生成

目录

0.前言

1.回顾什么是队列和栈

2.如何用两个队列实现一个栈

2.1思路讲解

2.2按照思路实现仿生栈的各接口

2.2.1栈的初始化

2.2.2栈的销毁

2.2.3栈的插入

2.2.4栈的删除

2.2.5 栈的栈顶数据

2.2.6 判断当前栈是否为空

3.如何用两个栈实现一个队列

3.1 思路分析

3.2代码实现

3.2.1 封装该队列

3.2.2队列的初始化

3.2.3队列的销毁

3.2.4队列的插入

3.2.5队列的队头

3.2.6队列的删除

3.2.7队列的判空


0.前言

4栈和队列OJ题集合 · onlookerzy123456qwq/data_structure_practice_primer - 码云 - 开源中国 (gitee.com)https://gitee.com/onlookerzy123456qwq/data_structure_practice_primer/tree/master/4%E6%A0%88%E5%92%8C%E9%98%9F%E5%88%97OJ%E9%A2%98%E9%9B%86%E5%90%88本文所有代码都已上传至gitee,可方便自取。

1.回顾什么是队列和栈

队列其实是先进先出,后进后出,一端负责入,一端负责出,先进入队列的元素,堵在后入队列的元素前面,必须前面的元素出了之后,后面的元素才能出。

请记住队列中元素的出入逻辑,先出的是当前最先入队列的元素。

栈其实是后进先出,先进后出,在同一端进行插入和删除,先入栈的元素,被后入栈的元素压在上面,必须上面后入栈的元素先出,下面这个先入栈的元素才能出。

请记住队列中元素的出入逻辑,先出的是当前最先入队列的元素。

 

2.如何用两个队列实现一个栈

225. 用队列实现栈 - 力扣(LeetCode)

2.1思路讲解

用两个队列模拟实现一个栈,其实就是如何仿生实现,让先入的数据后出,让后入的数据先出。

我们可以始终保持用一个队列装数据(非空队列),一个队列不装数据(空队列)

插入数据的时候,我们在非空队列进行插入数据,当然队列的插入是在队尾的插入

当要获取栈顶数据的时候,我们肯定是要出那个最近一次插入的数据,这样才是栈。我们最近一次插入的那个数据是在当前非空队列的队尾,所以我们直接返回非空队列的QueueBack即可。

当要删除数据的时候,我们栈的删除,是要删除最近一次插入的数据,这才是栈。而最近一次插入的数据,在非空队列的队尾。然而,队列的删除只能从队头删,这时候我们就要借助另一个不装数据的队列了,我们可以把这个非空队列中的元素出列,都依次装到另一个空队列当中直至我这个队列出到只剩下一个元素,这个元素就是我们要找的队尾元素,也即最近一次插入的元素,我们对这个元素删除即可!而且到新队列中的数据仍然保持原来的顺序。

例如非空队列里有1 2 3 4,所以依次插入的顺序是1 2 3 4,要删除的是最近一次插入的4,所以我们依次出1,放入空队列中,出2,放入空队列中,出3,放入空队列中,此时空队列里还是按序存储着1 2 3,非空队列里只剩下4,我们删除即可。

这是从逻辑上进行讲解,展示在代码上,就是我们用两个队列封装一个成栈的各种使用接口。

2.2按照思路实现仿生栈的各接口

在C语言当中,我们必须自己造轮子,我们其实没有一个实际意义上的一个队列,我们首先在OJ当中先实现一个Queue,详情可以看这篇博客:【面向小白】你见过这样讲解队列的吗?(阅此文可学会用纯C手撕一个队列)_yuyulovespicy的博客-CSDN博客

当前这个栈的本质就是两个队列,所以我们定义这个struct myStack类,用两个队列就可以将之代表。

typedef struct {
    Queue _q1;
    Queue _q2;
} MyStack;

然后我们在OJ当中按照我们2.1的思路实现各接口。

2.2.1栈的初始化

任何一个数据结构对象,都要完成初始化,否则你定义出来的指针成员就是野指针,否则你定义出来的int char等类型变量都是随机值。我们当前栈的本质是两个Queue,所以当前栈类的初始化函数就是对队列初始化接口的套壳。

但是注意这里使用创建栈的接口,myStackCreate接口的返回值是MyStack类对象的指针,也就是这个接口的本意,是想让我们把这个栈对象定义在堆区,然后返回这个对象的指针。malloc出来之后,我们就要对其内部的两个队列成员进行初始化

MyStack* myStackCreate() {
    //在堆区创建两个队列
    MyStack* ptmp = (MyStack*)malloc(sizeof(MyStack));
    //分别对两个队列完成初始化
    QueueInit(&(ptmp->_q1));
    QueueInit(&(ptmp->_q2));
    return ptmp;
}

2.2.2栈的销毁

MyStack的本质是两个队列Queue,队列也是在堆区中开辟的空间,比如我们是链式队列,在堆区就有许多的节点需要释放。每次使用完当前栈,都必须要释放堆区空间,否则会导致内存泄漏。

但是注意这里有三块堆区空间。

这里注意释放顺序,我们要首先释放栈指针obj指向的两个队列管理的堆区空间,然后再释放obj指向的Queue对象(即开辟在堆区的指针变量们)。不然释放顺序颠倒的话,我们要释放的是这些开在堆区的指针,我们就无法访问这两个指针了!!!那指针指向的堆区链式队列就丢了!!!


void myStackFree(MyStack* obj) {
    //释放所有在堆区的空间
    //包括MyStack的在堆区的2*2个指针,以及在堆区的链表队列空间
    QueueDestroy(&(obj->_q1));
    QueueDestroy(&(obj->_q2));
    free(obj);
}

2.2.3栈的插入

我们直接对非空队列进行插入即可。

这里介绍一个方法,我们的队列1和队列2,现在谁是空队列,谁是非空队列并不确定,如果我们 if(队列1为空) ......... else(队列2为空) .........,就需要我们写两段代码插入逻辑,会造成代码的冗余,所以我们运用指针假定法,进行判断与修正,最后两个指针,一个指向的就是非空队列,另一个指向的就是空队列。

//obj是栈的指针
void myStackPush(MyStack* obj, int x) {
    //给非空的队列进行插入
    //1.区分空队列/非空队列
    Queue* p_empty_queue = &(obj->_q1);//取出两个队列的指针
    Queue* p_no_empty_queue = &(obj->_q2);
    if(QueueEmpty(p_no_empty_queue))
    {
        p_empty_queue = &(obj->_q2);
        p_no_empty_queue = &(obj->_q1);
    }
    //现在两个指针指向的就是对应的空/非空队列
    //2.给非空队列进行插入
    QueuePush(p_no_empty_queue,x);
}

2.2.4栈的删除

把非空队列出数据到另一个空队列,直至只剩下一个数据,然后删除这个数据即可。

int myStackPop(MyStack* obj) {
    //让非空队列一直出数据到另一个空队列,直至只剩下一个数据
    //0.无有效数据空队列不可以进行删除
    assert(!myStackEmpty(obj));
    //1.区分空队列/非空队列
    Queue* p_empty_queue = &(obj->_q1);//取出两个队列的指针
    Queue* p_no_empty_queue = &(obj->_q2);
    if(QueueEmpty(p_no_empty_queue))
    {
        p_empty_queue = &(obj->_q2);
        p_no_empty_queue = &(obj->_q1);
    }
    //2.非空队列出数据到空队列,直至剩一个,对该个数据进行删除
    while(QueueSize(p_no_empty_queue)>1)
    {
        QueuePush(p_empty_queue,QueueFront(p_no_empty_queue));
        QueuePop(p_no_empty_queue);
    }
    int stacktop = QueueFront(p_no_empty_queue);
    QueuePop(p_no_empty_queue);
    return stacktop;
}

2.2.5 栈的栈顶数据

栈顶就是最近一次插入的数据,非空队列的back队尾就是最近一次插入的数据。

int myStackTop(MyStack* obj) {
    //栈顶就是最近一次插入的数据,即非空队列的back队尾
    //0.无有效数据空队列不可以进行删除
    assert(!myStackEmpty(obj));
    //1.区分空队列/非空队列
    Queue* p_empty_queue = &(obj->_q1);//取出两个队列的指针
    Queue* p_no_empty_queue = &(obj->_q2);
    if(QueueEmpty(p_no_empty_queue))
    {
        p_empty_queue = &(obj->_q2);
        p_no_empty_queue = &(obj->_q1);
    }
    return QueueBack(p_no_empty_queue);
}

2.2.6 判断当前栈是否为空

栈的初始空状态就是,两个队列都是空。

bool myStackEmpty(MyStack* obj) {
    return QueueEmpty(&(obj->_q1))&&QueueEmpty(&(obj->_q2));
}

3.如何用两个栈实现一个队列

232. 用栈实现队列 - 力扣(LeetCode)

3.1 思路分析

我们先定义两个栈,一个叫PushSt,一个叫PopSt。

思路是类似的,队列是先进的数据,即先插入的数据先出。后进的数据必须等前面的数据出去之后,才能出。

假设我们往栈里依次插入1 2 3 4 5,但是队列要求的是先插入的元素先出,所以说我们下一次应该出的应该是栈底的1,而不是栈顶的5,那怎么倒腾出这个1呢?肯定是要借助另一个栈,我们只要把这个被插入数据的栈,依次出栈(5 4 3 2 1),入栈到另一个栈当中,那此时栈顶的就是1,从栈顶到栈底是1 2 3 4 5(完成了反序)。

 反序之后,此时PopSt栈顶的数据就是最先插入的数据了!!!我们的思路就是这样,在一个PushSt中正序插入,然后倒腾到PopSt当中完成反序,这样在PopSt中,依次出栈的顺序就是先插入的数据-->后插入的数据(如图)。

那反序之后,我们再插入数据是在PushSt当中,还是PopSt当中呢?当然是在PushSt中!一旦插入到PopSt当中,例如我们现在依次把6 7 8插入到PopSt当中,那顺序就全部乱套了!

 

我们要始终维护住PopSt当中的数据,插入的时序上都是早于PushSt当中的所有数据,并且PopSt中元素的出栈顺序就是从最先到最后插入的顺序,这样才满足队列的性质嘛!

所以当插入的时候我们无脑把数据插入到PushSt当中

删除或者取出的数据的时候如果PopSt不为空,那PopSt栈顶的数据就是我们想要的数据如果PopSt为空,那我们就把PushSt当中的数据全部都倒腾到PopSt当中即可。

3.2代码实现

首先我们首先造一个轮子,即手撕一个栈进行使用详情可以参考下面这篇博客:[面向小白]一篇博客带你认识什么是栈以及如何手撕一个栈_yuyulovespicy的博客-CSDN博客

3.2.1 封装该队列

该队列类,是通过两个栈实现的,所以我们封装两个栈成员,即可代表这个队列。

typedef struct {
    //正序栈 负责接收插入的数据 
    //反序栈 负责删除拿出的数据
    Stack _forward_st;
    Stack _reverse_st;
} MyQueue;
/*
forward_st  reverse_st
                1
    7           2
    6           3
    5           4
*/

3.2.2队列的初始化

在堆区创建这个栈对象。

MyQueue* myQueueCreate() {
    //在堆区开辟两个栈
    MyQueue* ptmp = (MyQueue*)malloc(sizeof(MyQueue));
    //对栈进行初始化
    StackInit(&(ptmp->_forward_st));
    StackInit(&(ptmp->_reverse_st));
    return ptmp;
}

3.2.3队列的销毁

清理所有的堆区资源,两个栈实体的资源,以及定义在堆区的队列成员。

void myQueueFree(MyQueue* obj) {
    //释放所有在堆区的空间
    //包括在堆区的两个栈st实体,以及栈管理的顺序表实体
    StackDestroy(&(obj->_reverse_st));
    StackDestroy(&(obj->_forward_st));
    free(obj);
}

3.2.4队列的插入

直接在PushSt,即正序栈_forward_st当中进行插入。

void myQueuePush(MyQueue* obj, int x) {
    //直接在正序栈中进行(栈顶)插入
    StackPush(&(obj->_forward_st),x);
}

3.2.5队列的队头

先插入的元素在PopSt当中,后插入的元素在PushSt当中,并且PopSt当中的数据都是从PushSt反序倒腾来的,从PopSt的栈顶到栈底,其顺序就是满足队列先入先出的性质。所以我们直接获取PopSt的栈顶即可。

当然如果现在PopSt是空,那么我们就要首先把PushSt中的所有数据反序倒腾到PopSt当中。

int myQueuePeek(MyQueue* obj) {
    //获取队头的元素
    //我们直接在反序栈进行返回(栈顶元素)即可
    //如果反序栈是空,就需要先对正序栈中的所有数据倒过来,再进行返回。
    if(StackEmpty(&(obj->_reverse_st)))
    {
        while(!StackEmpty(&(obj->_forward_st)))
        {
            int forward_top = StackTop(&(obj->_forward_st));
            StackPush(&(obj->_reverse_st),forward_top);
            StackPop(&(obj->_forward_st));
        }
    }
    return StackTop(&(obj->_reverse_st));
}

3.2.6队列的删除

就是找到到当前最先插入的数据,即找到队头的数据的基础上,并进行删除即可。

int myQueuePop(MyQueue* obj) {
    //0.首先必须有数据才能删除
    assert(!myQueueEmpty(obj));
    //我们直接在反序栈进行删除(栈顶)即可
    //如果反序栈是空,就需要先对正序栈中的所有数据倒过来,再进行删除。
    if(StackEmpty(&(obj->_reverse_st)))
    {
        while(!StackEmpty(&(obj->_forward_st)))
        {
            int forward_top = StackTop(&(obj->_forward_st));
            StackPush(&(obj->_reverse_st),forward_top);
            StackPop(&(obj->_forward_st));
        }
    }
    //需要返回删除的队头数据
    int front = StackTop(&(obj->_reverse_st));
    StackPop(&(obj->_reverse_st));
    return front;
}

3.2.7队列的判空

很简单,我们所有的有效数据都是存储在两个栈当中的,所以只要两个栈都是空,那么这个队列就是空。

bool myQueueEmpty(MyQueue* obj) {
    return StackEmpty(&(obj->_forward_st))&&StackEmpty(&(obj->_reverse_st));
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值