算法题C——用队列实现栈/用栈实现队列

文章目录

    • (一)用队列实现栈
      • 详细分析
        • [(1)创建两个队列用于实现栈的结构体 MyStack](#(1)创建两个队列用于实现栈的结构体 MyStack)
        • [(2)初始化 myStackCreate()](#(2)初始化 myStackCreate())
        • [(3)入栈插入函数 myStackPush](#(3)入栈插入函数 myStackPush)
        • [(4)移除并返回栈顶元素函数 myStackPop](#(4)移除并返回栈顶元素函数 myStackPop)
        • [(5)返回栈顶元素函数 myStackTop](#(5)返回栈顶元素函数 myStackTop)
        • [(6)判空 myStackEmpty](#(6)判空 myStackEmpty)
        • [(7)栈的销毁 myStackFree (队列的销毁+MyStack的销毁)](#(7)栈的销毁 myStackFree (队列的销毁+MyStack的销毁))
        • (8)代码汇总
    • (二)用栈实现队列
      • 详细分析
        • [(1)创建两个栈用于实现队列的结构体 MyQueue](#(1)创建两个栈用于实现队列的结构体 MyQueue)
        • [(2) 初始化 myQueueCreate](#(2) 初始化 myQueueCreate)
        • [(3)尾插入队 myQueuePush](#(3)尾插入队 myQueuePush)
        • [(4)返回并头删出队 myQueuePop](#(4)返回并头删出队 myQueuePop)
        • [(5)返回队头元素 myQueuePeek](#(5)返回队头元素 myQueuePeek)
        • [(6)判空 myQueueEmpty](#(6)判空 myQueueEmpty)
        • [(7)销毁 myQueueFree](#(7)销毁 myQueueFree)
        • (8)代码汇总

(一)用队列实现栈

用队列实现栈

思路:其实思路挺简单的,两个栈,保证一个时常为空,保证另一个不为空用于push数据(入栈),在空队列和非空队列之间来回导数据之时,就能完成队列实现栈的各个函数的定义

详细分析

(1)创建两个队列用于实现栈的结构体 MyStack
c 复制代码
//第一种定义方式
typedef struct {
    QNode q1;
    QNode q2; 
} MyStack;
c 复制代码
//第二种定义方式
typedef struct {
    Queue q1;
    Queue q2;
} MyStack;

实际上,第二种定义方式才是对的。

只有将两套头尾结点指针封装成一个结构体,才能和后面相关函数的参数相符。

后面的函数都需要两套头尾结点的二级指针(4个二级指针),但只传了一个一级指针参数obj,只有传递包含两套头尾结点指针的结构体的指针,才能取到需要的四个二级指针。

(2)初始化 myStackCreate()

一定要记住:初始化的前提是变量已经存在了空间.在这里,我们初始化的是已经有了空间的包含两套头尾结点指针的结构体,而不是节点!

c 复制代码
MyStack* myStackCreate() {
    //没有传参意味着需要在函数内创建变量,但在栈中申请局部变量,出函数就销毁了
    //所以需要在堆中malloc
    MyStack* obj = (MyStack*)malloc(sizeof(MyStack));
    QueueInit(&(obj->q1));
    QueueInit(&(obj->q2));
    //malloc为包含两套头尾结点指针的结构体分配了空间
    //所以是对头尾节点指针进行初始化
    //而不是对还未分配空间的节点进行初始化
    return obj;
}
(3)入栈插入函数 myStackPush

非空队列是实现的栈,空队列是用来导值的。

想要入队栈顶插入,首先要找到非空队列。

c 复制代码
void myStackPush(MyStack* obj, int x) {
    if(!QueueEmpty(&(obj->q1)))
    {
       QueuePush(&(obj->q1),x);
    }
    else
    {
        QueuePush(&(obj->q2),x);
    }
}
(4)移除并返回栈顶元素函数 myStackPop

实现思路:将非空队列中size-1个值都导入空队列中(队列中的数据只能从队头出去,队头相当于栈底),最后再取出原先的非空队列的最后一个值,这个值就是原先非空队列队尾的值,队尾相当于栈顶,也就是说,最后一个值就是我们需要的栈顶元素。

c 复制代码
int myStackPop(MyStack* obj) {
    //导值留一个
    //将非空队列的数据导入空队列中,但并不知道哪个队列为空
    //需要先分两种情况,再在每种情况下写相同逻辑的语句
    //假设法
    Queue* empty = &(obj->q1); 
    Queue* noempty = &(obj->q2);
    if(QueueEmpty(&(obj->q2)))
    {
        empty = &(obj->q2);
        noempty = &(obj->q1);
    }
    //将非空队列的数据导入空队列中
    while(QueueSize(noempty)>1)
    {
        //留出一个不导
        QDatatype top =        QueueFront(noempty);
        QueuePush(empty,top);
        QueuePop(noempty);

    }
    QDatatype top = QueueFront(noempty);
    QueuePop(noempty);
    return top;
}
坑:

①假设法可不可以写成:

c 复制代码
   Queue empty = obj->q1; 
   Queue noempty = obj->q2;
   if(QueueEmpty(&(obj->q2)))
  {
        empty = obj->q2;
        noempty = obj->q1;
  }

绝对不可以!如果仅仅是将q1q2的值赋给了empty、noempty,传&empty、&noempty给QueueFront,QueuePush,QueuePop之后,是会对头尾指针有影响的,可这个影响只会改变empty和noempty的值,并不会改变q1q2的值,这就会发生错误。

完成myStackPop函数之后,就会执行下一个函数,那个时候传的q1q2还是原先的值,但实际上在执行完myStackPop函数之后,q1q2的值就已经变了。

所以我们应该取q1q2的地址,再赋给empty、noempty,这样就能通过地址找到q1q2并对其进行更新了。

本质上就是:当我们要通过一个变量来改动另一个变量时,就要给变量赋待改动变量的地址!

②while循环条件能不能换成for(int i = 0;isize-1;i++)?

c 复制代码
    for(int i = 0;i<noempty->size-1;i++)
    {
        QDatatype top = QueueFront(noempty);
        QueuePush(empty,top);
        QueuePop(noempty);
    }

绝对不可以!

在QueuePop和QueuePush函数中,size的值会发生变化,如果使用for循环,则i在++的同时noempty队列的size个数也在--,那么循环的次数就会出错。

可要是使用while循环且借助数个数函数就不会出错,noempty队列中挪一个元素则元素总个数减1,当只剩下一个时,就满足我们想留下一个数据不导的要求了。

③既然有取队尾函数QueueBack,为啥不直接取数据,还要导来导去?

关键在于:确实能从非空队列中取队尾数据,但是并不能删除队尾的数据。

数据只能从队头出队,不能从队尾出队,而取队尾只是找到了队尾元素并返回,而不是实现了出队操作。

所以两个队列间来回导的目的不是为了找到队尾元素,而是为了实现在队尾删除元素的操作,也就是这里出栈的操作。

(5)返回栈顶元素函数 myStackTop

在栈中有相关的取队尾元素函数Queueback,直接调用就行,这里不需要让数据在两个队列之间导来导去。

既然是取数据,那就要先判断哪个是非空队列,才能进去取队尾元素。

c 复制代码
int myStackTop(MyStack* obj) {

    if(QueueEmpty(&(obj->q1)))
    {
     return QueueBack(&(obj->q2));
    }
    else
    {
      return QueueBack(&(obj->q1));
    }
}
(6)判空 myStackEmpty

只有两个队列里都没有数据了,才算实现的栈中没有数据。

c 复制代码
bool myStackEmpty(MyStack* obj) {
    return QueueEmpty(&(obj->q1)) && QueueEmpty(&(obj->q2));
}
(7)栈的销毁 myStackFree (队列的销毁+MyStack的销毁)

首先要明白,需要手动销毁(回收释放)的是堆中的空间,在这里,使用malloc动态开辟的是队列的空间,以及用于存储两套头尾结点指针的结构体Mystack,释放空间要由内而外。

我们来梳理一下内存的开辟是怎样的。

首先,仅仅只是定义结构体是不会为结构体分配空间的,所以真正分配空间是从指针obj开始的,创建一个在栈上的局部指针变量,存的地址指向在堆中为MyStack分配的空间,MyStack结构体有空间后,这份空间就会分配给它的成员变量q1q2(也就是说结构体有空间,对应成员变量也就有空间),而MyStack结构体的成员变量q1q2同样也是结构体,则q1q2的成员变量phead、ptail也在堆中有了空间。等到QueuePush函数中,想进行插入操作就需要创建新节点,这个时候才会为队列节点分配堆中的空间(红色和黑色方块)。

回收释放的顺序是由内而外:因为假如我们先释放了外层的MyStack结构体的空间,那成员变量q1q2就找不到了,相应的指向节点的phead指针也找不到了,这样我们就无法回收释放节点(红黑方块)的空间了。

c 复制代码
void myStackFree(MyStack* obj) 
{
      QueueDestory(&(obj->q2));
      QueueDestory(&(obj->q1));
      free(obj);
      obj = NULL;
}
(8)代码汇总

(为避免代码太长,实现队列极其相关函数的代码并没有张贴出来,如果对此存在疑问,可以去看看我的另一篇博客------单链表实现队列!)

c 复制代码
typedef struct {
    Queue q1;
    Queue q2;
    
} MyStack;

MyStack* myStackCreate() {
    //没有传参意味着需要在函数内创建变量,但在栈中申请局部变量,出函数就销毁了
    //所以需要再堆中malloc
    MyStack* obj = (MyStack*)malloc(sizeof(MyStack));
    QueueInit(&(obj->q1));
    QueueInit(&(obj->q2));
    //malloc为包含两套头尾结点指针的结构体分配了空间
    //所以是对头尾节点指针进行初始化
    //而不是对还未分配空间的节点进行初始化
    return obj;
    
}

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

int myStackPop(MyStack* obj) {
    //导值留一个
    //将非空队列的数据导入空队列中,但并不知道哪个队列为空
    //需要先分两种情况,再在每种情况下写相同逻辑的语句
    //假设法
    Queue* empty = &(obj->q1); 
    Queue* noempty = &(obj->q2);
    if(QueueEmpty(&(obj->q2)))
    {
        empty = &(obj->q2);
        noempty = &(obj->q1);
    }
    //将非空队列的数据导入空队列中
    while(QueueSize(noempty)>1)
    {
        //留出一个不导
        QDatatype top = QueueFront(noempty);
        QueuePush(empty,top);
        QueuePop(noempty);

    }
    QDatatype top = QueueFront(noempty);
    QueuePop(noempty);
    return top;
}

int myStackTop(MyStack* obj) {

    if(QueueEmpty(&(obj->q1)))
    {
     return QueueBack(&(obj->q2));
    }
    else
    {
      return QueueBack(&(obj->q1));
    }
}

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

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

(二)用栈实现队列

用栈实现队列

思路:同样是创建两个栈,一个用于push数据,一个用于pop数据,在倒值之间,就能实现相关函数。

详细分析

(1)创建两个栈用于实现队列的结构体 MyQueue
c 复制代码
typedef struct {
    ST pushst;
    ST popst;
} MyQueue;
(2) 初始化 myQueueCreate
c 复制代码
MyQueue* myQueueCreate() {
    //没有传参,且避免局部变量出函数销毁,所以malloc
    MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));
    STInit(&(obj->pushst));
    STInit(&(obj->popst));
    return obj;
}
(3)尾插入队 myQueuePush

直接插入pushst栈中,不需要担心空间是否够,因为在STPush函数中存在空间检查的相关语句。

c 复制代码
void myQueuePush(MyQueue* obj, int x) {
    //尾插入队 入栈
    STPush(&(obj->pushst),x); 
}

或许你会问:此处不需要找空栈再进行插入吗??

其实原因很简单。

在先前的两个队列实现栈中,我们要来回导数据,是因为队列无法进行尾删,实现不了栈的删除操作,所以需要通过导数据的方式来实现我们每次进行删除操作都需要导数据,为了防止在来回导数据之间昏头转向,我们就干脆让数据只能插入非空队列,这样我们导数据的方向就定死了------只能从非空队列导向空队列。

所以插入需要找空队列,删除要找非空队列。

而在两个栈实现队列当中,我们需要倒数据,是因为栈无法实现栈底删除,实现不了队列的头删出队操作。数据需要入栈,可一个栈又不能实现栈底删除操作,所以我们又想到了倒值,假设是第一次需要头删出队,也是第一次做倒值操作,可以知道,数据从pushst出栈,再入popst,顺序是反的,而"颠倒"就是我们想要的,这时再对popst栈进行出栈删除操作,删去的值也就是原先pushst栈中的栈底元素,也就实现了队列头删出队。

当到第二次需要头删出队时,如果先前进行倒值进入popst中的值还有剩余,我们就直接对popst栈进行出栈删除操作就行了,因为倒值的时候,紧挨着pushst栈底的元素就是我们第二次像头删出队的数据啊。只有当popst栈空时,才需要再进行倒值操作。

到这,你应该对pushst和popst栈的作用有了更深的理解,也就明白为啥不用找空栈了。

(4)返回并头删出队 myQueuePop
c 复制代码
int myQueuePop(MyQueue* obj) {
    //移除并返回队头元素
    //将push栈中数据全部倒入pop栈中,再取pop栈的栈顶元素,就是队列的队头元素
    if(STEmpty(&(obj->popst)))
    {
        //如果pop栈为空了,就需要先倒值了
        while(!STEmpty(&(obj->pushst)))
        {
            STDatatype top = STTop(&(obj->pushst));
            STPush(&(obj->popst),top);
            STPop(&(obj->pushst));
        }
       
    }
        STDatatype top = STTop(&(obj->popst));
        STPop(&(obj->popst));
        return top;
}
(5)返回队头元素 myQueuePeek
c 复制代码
int myQueuePeek(MyQueue* obj) {
    //返回队头数据
    //仅仅只是返回,不删除
     if(STEmpty(&(obj->popst)))
    {
        //如果pop栈为空了,就需要先倒值了
        while(!STEmpty(&(obj->pushst)))
        {
            STDatatype top = STTop(&(obj->pushst));
            STPush(&(obj->popst),top);
            STPop(&(obj->pushst));
        }
    }
     return STTop(&(obj->popst));
}

写完peek和pop函数,会发现其实二者很相似,只是前者只是返回,而后者在返回的基础上还要删除,所以我们可以在pop函数内部调用peek函数,这样能让程序更简洁。

代码优化
c 复制代码
int myQueuePeek(MyQueue* obj) {
    //返回队头数据
    //仅仅只是返回,不删除
     if(STEmpty(&(obj->popst)))
    {
        //如果pop栈为空了,就需要先倒值了
        while(!STEmpty(&(obj->pushst)))
        {
            STDatatype top = STTop(&(obj->pushst));
            STPush(&(obj->popst),top);
            STPop(&(obj->pushst));
        }
    }
     return STTop(&(obj->popst));
}

int myQueuePop(MyQueue* obj) {
    //移除并返回队头元素
    //将push栈中数据全部倒入pop栈中,再取pop栈的栈顶元素,就是队列的队头元素
    STDatatype top = myQueuePeek(obj);
    STPop(&(obj->popst));
    return top;
}

在调用peek函数前得有peek函数的定义,如果peek函数出现在pop函数之后提交报错,就需要前置声明。

(6)判空 myQueueEmpty
c 复制代码
bool myQueueEmpty(MyQueue* obj) {
    return STEmpty(&(obj->pushst))&&STEmpty(&(obj->popst));
}
(7)销毁 myQueueFree
c 复制代码
void myQueueFree(MyQueue* obj) {
    STDestory(&(obj->popst));
    STDestory(&(obj->pushst));
    free(obj);
    obj = NULL;
}
(8)代码汇总
c 复制代码
typedef struct {
    ST pushst;
    ST popst;
} MyQueue;


MyQueue* myQueueCreate() {
    MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));
    STInit(&(obj->pushst));
    STInit(&(obj->popst));
    return obj;
}

void myQueuePush(MyQueue* obj, int x) {
    //尾插入队 入栈
    STPush(&(obj->pushst),x);
    
}

int myQueuePeek(MyQueue* obj) {
    //返回队头数据
    //仅仅只是返回,不删除
     if(STEmpty(&(obj->popst)))
    {
        //如果pop栈为空了,就需要先倒值了
        while(!STEmpty(&(obj->pushst)))
        {
            STDatatype top = STTop(&(obj->pushst));
            STPush(&(obj->popst),top);
            STPop(&(obj->pushst));
        }
    }
     return STTop(&(obj->popst));
}

int myQueuePop(MyQueue* obj) {
    //移除并返回队头元素
    //将push栈中数据全部倒入pop栈中,再取pop栈的栈顶元素,就是队列的队头元素
    // if(STEmpty(&(obj->popst)))
    // {
    //     //如果pop栈为空了,就需要先倒值了
    //     while(!STEmpty(&(obj->pushst)))
    //     {
    //         STDatatype top = STTop(&(obj->pushst));
    //         STPush(&(obj->popst),top);
    //         STPop(&(obj->pushst));
    //     }
       
    // }
    //     STDatatype top = STTop(&(obj->popst));
    //     STPop(&(obj->popst));
    //     return top;

    STDatatype top = myQueuePeek(obj);
    STPop(&(obj->popst));
    return top;
}



bool myQueueEmpty(MyQueue* obj) {
    return STEmpty(&(obj->pushst))&&STEmpty(&(obj->popst));
}

void myQueueFree(MyQueue* obj) {
    STDestory(&(obj->popst));
    STDestory(&(obj->pushst));
    free(obj);
    obj = NULL;
}

------end------

相关推荐
谭欣辰2 小时前
详细讲解 C++ 有向无环图(DAG)及拓扑排序
c++·算法·图论
承渊政道2 小时前
【递归、搜索与回溯算法】(掌握记忆化搜索的核心套路)
数据结构·c++·算法·leetcode·macos·动态规划·宽度优先
闻缺陷则喜何志丹2 小时前
【 线性筛 调和级数】P7281 [COCI 2020/2021 #4] Vepar|普及+
c++·算法·洛谷·线性筛·调和级数
zzzsde2 小时前
【Linux】线程概念与控制(1)线程基础与分页式存储管理
linux·运维·服务器·开发语言·算法
穿条秋裤到处跑2 小时前
每日一道leetcode(2026.04.23):等值距离和
算法·leetcode·职场和发展
少许极端2 小时前
算法奇妙屋(四十九)-贡献法
java·算法·leetcode·贡献法
叶子野格2 小时前
《C语言学习:数组》11
c语言·开发语言·c++·学习·visual studio
武帝为此2 小时前
【特征选择方法】
算法·数学建模
Little At Air2 小时前
C++priority_queue模拟实现
开发语言·数据结构·c++