目录
0.前言
1.回顾什么是队列和栈
队列其实是先进先出,后进后出,一端负责入,一端负责出,先进入队列的元素,堵在后入队列的元素前面,必须前面的元素出了之后,后面的元素才能出。
请记住队列中元素的出入逻辑,先出的是当前最先入队列的元素。
栈其实是后进先出,先进后出,在同一端进行插入和删除,先入栈的元素,被后入栈的元素压在上面,必须上面后入栈的元素先出,下面这个先入栈的元素才能出。
请记住队列中元素的出入逻辑,先出的是当前最先入队列的元素。
2.如何用两个队列实现一个栈
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.如何用两个栈实现一个队列
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));
}