栈与队列的数据存储方式完全不同,栈的数据遵循先进后出模式FILO,而队列为先进先出模式FIFO,要想使用栈的结构实现队列的数据增删模式,需要使用栈的性质并对其稍加巧用,就可以达到同队列的数据存储访问相同的效果。
注意,本章中用栈实现队列所用到的栈函数,以及队列实现栈使用到的队列接口函数都在上一章模拟实现提及到,详情请参照上一章,链接在此数据结构——栈和队列_VelvetShiki_Not_VS的博客-CSDN博客。
定义两个栈,一个用于临时存放压栈的数据,命名其为Push栈,再定义一个专用于出数据的栈Pop,当数据压入时,全部压入Push栈,只要遇到出栈命令,就将Push栈的所有数据取顶并全部倒入Pop栈中,此时的Pop栈数据为Push栈数据的逆序,即如果Push栈的数据压入为1,2,3,4,分别取顶为4,3,2,1并压入Pop栈,此时Pop栈由栈顶自栈底的数据依次为1,2,3,4,如果全部出栈则刚好与数据的入栈顺序相同(即入栈1,2,3,4,出栈也为1,2,3,4的顺序)。
双栈指针的结构定义
栈的基本结构定义遵循顺序表的定义方式,包含存储数据的数值域,标识栈顶下标的整型top,标识栈容量的整型capacity,相关栈函数接口可参考上述栈和队列链接,本章的栈转换队列算法引用的栈接口函数不再详细赘述。
//栈定义
typedef int STEtype;
typedef struct Stack
{
STEtype* arr; //栈通过顺序表实现,定义可扩容数组,容量和顶部
int top;
int capacity;
}ST;
//两个栈底指针的结构体
typedef struct MyQueue
{
ST* Push; //Push栈用于数据压入的临时存放
ST* Pop; //当需要出队时,将Push栈的数据取头倒入Pop栈,并逐个取头出队
}MQ;
栈指针结构体初始化函数
MQ* StructInit() //初始化栈指针
{
MQ* Stacks = (MQ*)malloc(sizeof(MQ));
Stacks->Push = StackInit();
Stacks->Pop = StackInit();
return Stacks;
}
在队列一章我们曾使用链表的方式实现队列数据的入队,而使用顺序表的方式同样可是实现入队操作,此处因为需使用栈的空间结构实现队列功能,实质上也使用了顺序表的方式入队。对于队列的“压栈”操作,只需根据需求将需要入队的数据全部压到Push栈中即可,原理图如下:
队列压栈函数
void Push(MQ* obj, STEtype x)
{
assert(obj);
StackPush(obj->Push, x); //调用了栈自身的压栈函数
}
使用栈入队与压栈的方式大体相同,因为不需要对数据进行过多的操作,直接将数据存入即可,如果此时直接从Push栈里将数据弹出,就是栈的出栈方式。但如果想要模拟队列的出队方式,需要对栈的弹出做些文章,原理图如下:
栈出队函数
void Pop(MQ* obj)
{
assert(obj); //检查两个栈底指针有效性
if (StackEmpty(obj->Pop)) //如果Pop栈不为空,则执行Push栈中数据的倒入
{
while (!StackEmpty(obj->Push)) //如果Push栈不为空,则继续将数据取顶倒入Pop,再将Push顶依次弹栈
{
StackPush(obj->Pop, StackTop(obj->Push));
StackPop(obj->Push);
}
}
StackPop(obj->Pop); //将位于Pop栈顶元素弹出
}
一定要记住,要符合栈的队列出队性质,数据的入队只能从Push栈一端进入,而数据的出队只能从Pop栈一端弹出。
对于双栈结构的队列取队头元素,就是取非空Pop栈的栈顶元素,如果仅有Pop栈为空,Push栈非空,则从Push栈元素全部取顶压入Pop栈再取顶(Pop不弹栈);如果两栈均空,则由栈取顶函数StackTop中判断栈为空,返回无意义值-1。
栈取队头函数
STEtype Peek(MQ* obj) //取队头元素
{
assert(obj);
if (StackEmpty(obj->Pop))
{
while (!StackEmpty(obj->Push))
{
StackPush(obj->Pop, StackTop(obj->Push));
StackPop(obj->Push);
}
}
return StackTop(obj->Pop);
}
测试用例
MQ* MyQ = StructInit();
printf("%d", Peek(MyQ));
Push(MyQ, 1);
printf("队头元素为:%d\n", Peek(MyQ));
Push(MyQ, 2);
printf("队头元素为:%d\n", Peek(MyQ));
Push(MyQ, 3);
printf("队头元素为:%d\n", Peek(MyQ));
Push(MyQ, 4);
printf("队头元素为:%d\n", Peek(MyQ));
Pop(MyQ);
printf("队头元素为:%d\n", Peek(MyQ));
观察结果
对于栈和队列的结构已知,如果要对这两种特殊的线性结构进行数值遍历,则需要清空栈或队列的元素,将元素全部弹栈或出队,这样遍历完成后栈或队列均为空。对于使用栈结构模拟的队列亦是如此。
遍历函数
void PrintQueue(MQ* obj)
{
assert(obj);
printf("Head-> ");
while (!StackEmpty(obj->Pop)) //如果Pop栈不为空,将Pop栈元素循环取顶并弹栈
{
printf("%d ", Peek(obj));
Pop(obj);
}
if (!StackEmpty(obj->Push)) //此时Pop栈一定为空,再判断Push栈是否为空
{
Peek(obj); //如果非空,则将Push栈中元素循环取顶并压入Pop栈
while (!StackEmpty(obj->Pop))
{
printf("%d ", Peek(obj)); //再将从Push栈中压入Pop栈的数据依次取顶并弹栈
Pop(obj);
}
}
printf("<-Tail\n");
}
测试用例
//双栈初始化为空
MQ* MyQ = StructInit();
//入队&出队
Push(MyQ, 1);
Push(MyQ, 2);
Push(MyQ, 3);
Pop(MyQ);
Push(MyQ, 4);
Push(MyQ, 5);
Push(MyQ, 6);
Push(MyQ, 7);
Pop(MyQ);
//两次队列遍历
PrintQueue(MyQ);
PrintQueue(MyQ);
观察结果
Head-> 3 4 5 6 7 <-Tail
Head-> <-Tail
当两个栈均为空时,双栈构成的队列才为空。
判空函数
bool Empty(MQ* obj) //判断队列是否为空
{
assert(obj);
return StackEmpty(obj->Push) && StackEmpty(obj->Pop);
}
释放双栈构成的队列时,需要将先前开辟过的所有内存空间均释放,这些空间包括:
双栈的队列释放函数
MQ* Free(MQ* obj) //销毁队列
{
assert(obj); //检查双栈指针有效性
obj->Push = StackDestroy(obj->Push); //分别释放两个栈,顺序可以颠倒
obj->Pop = StackDestroy(obj->Pop);
free(obj); //再释放双栈结构体信息指针,置空后返回
obj = NULL;
return obj;
}
调试观察结果
在前一章中,我们使用了链表的方式实现了队列的基本结构,而使用队列的结构来实现栈,其思维逻辑与栈实现队列的大体相同,都是需要进行数据的倒入和交换。因为需要使用到队列的结构,所以队列的基本函数接口也可以参考前章中如上文的栈和队列链接,而本课题在此基础上实现对栈的数据存储和访问先进后出(FILO)的模拟。
使用双栈的队列模拟实现,使用了两个栈Push和Pop栈分别实现入队与数据倒入Pop栈再取顶弹出的方式实现出队操作。而要使用队列模拟栈的数据存储和访问方式,也需要使用到两个队列,分别进行结点数据的入队和出队,整体思路大致如下图所示:
可以看出,双队列的“压栈”操作与队列的入队几乎没有区别,都是将数据直接入队“压栈”即可,而在“弹栈”过程中,队列将数据的出队为了满足栈的性质,即最先存储的数据最后出队,而最后存储的数据反而最先出队,所以需要将队列最后入队的那个数据出队即可。但根据队列的性质,出队操作仅能对队头元素弹出,所以将一个非空队列中最后一个结点元素前面的所有结点都依次入队到另一个队列,再将最后一个元素出队,即可满足要求。
双队列结构体模拟栈
typedef int QEtype;
typedef struct Queue //基于链表结构的队列结构体定义
{
QEtype data;
struct Queue* next;
}QE;
//指向两个队列指针的指针
typedef struct MyStack
{
QE* Q1; //指向第一个队列
QE* Q2; //指向第二个队列
}MST;
双队列指针初始化函数
MST* StackCreate()
{
MST* Queues = (MST*)malloc(sizeof(MST)); //开辟包含队列指针信息的结构体空间初始化
Queues->Q1 = Queues->Q2 = NULL; //将两个指向Q1和Q2队列的指针初始化置空
return Queues;
}
双队列指针销毁函数
MST* Free(MST* obj)
{
assert(obj);
QueueDestroy(&obj->Q1); //将队列Q1和Q2销毁,即链表中所有结点的释放,并将队列指针置空
QueueDestroy(&obj->Q2);
free(obj); //将指向队列指针信息的结构体形参指针释放并置空,并返回给实参
obj = NULL;
return obj;
}
双队列判空函数:
bool Empty(MST* obj)
{
assert(obj);
return QueueEmpty(&(obj->Q1)) && QueueEmpty(&(obj->Q2));
}
从开头的原理图可看出,对于队列的压栈操作与入队基本一致,因为是基于链表对数据的存储,所以只需向非空队列的一端入队数据即可。
void Push(MST* obj, QEtype x)
{
assert(obj);
if (QueueEmpty(&obj->Q1)) //判断Q1是否为空,如果为空则保持指针指向不变,如果非空,则此时Q2一定为空,指针交换指向
{
QueuePush(&obj->Q2, x); //将新增数据入队到非空队列中(可能是Q1队列,也有可能是Q2队列)
}
else
{
QueuePush(&obj->Q1, x);
}
}
需要注意的是,使用双队列结构入队,入队的一端永远是非空队列,而另一个队列一定为空。不能对空队列进行入队操作,而空队列与非空队列不能确定具体是Q1或是Q2,因为在不断的入队与出队过程中,两个队列都分别可能对空队列或非空队列,但入队操作仅能在非空队列的一方进行。如果初始两个队列均为空,依据上述函数作if判断,当Q1为空时,默认Q2为非空队列(即使Q2队列本身也为空),向其中入队数据即可。
队列出队要遵循栈的弹栈规则,就必须让队列进行结点数据的迁移,让非空队列的队尾结点数据进行单独的出队“弹栈”。
双队列弹栈函数
void Pop(MST* obj)
{
assert(obj);
if (Empty(obj)) //如果双队列均为空,则无需出队
{
return;
}
QE** Empty = &(obj->Q1), ** NonEmpty = &(obj->Q2); //将两个队列指针取地址,分别赋值给定义的二级指针空和非空
if (!QueueEmpty(Empty)) //如果空指针指向队列不为空,则交换两个指针指向
{
Empty = &(obj->Q2);
NonEmpty = &(obj->Q1);
}
while (QueueSize(NonEmpty) > 1) //将非空队列的数据除队末结点元素外,全部压入空队列一方
{
QueuePush(Empty, QueueTop(NonEmpty));
QueuePop(NonEmpty);
}
QueuePop(NonEmpty); //最后将仅留下一个结点的原非空队末元素出队,同时此队列变为空队列
}
原理图如下
双队列结构栈的取栈顶元素函数也需要寻找空与非空队列,在队列结构中,非空队列的队尾元素是最后入队的,所以该元素即为栈的栈顶元素,遍历非空队列并对队尾元素取值返回即可。
双队列栈取顶函数
QEtype Top(MST* obj)
{
assert(obj);
if (Empty(obj)) //如果双队列均为空,则无栈顶元素可取
{
return NULL;
}
QE** Empty = &obj->Q1, ** NonEmpty = &obj->Q2;
if (!QueueEmpty(Empty)) //重新分配空与非空指向队列指针,原理与Pop函数交换一致
{
Empty = &obj->Q2;
NonEmpty = &obj->Q1;
}
QE* tail = *NonEmpty; //定义临时遍历找尾结点指针tail,对非空队列进行遍历取尾
while (tail->next)
{
tail = tail->next;
}
return tail->data; //找到末节点,返回结点数值域值,且不弹栈(不出队)
}
双队列的栈元素个数计算函数
int Size(MST* obj)
{
assert(obj);
if (Empty(obj))
{
return 0;
}
if (QueueEmpty(&obj->Q1))
{
return QueueSize(&obj->Q2);
}
return QueueSize(&obj->Q1);
}
栈或队列的遍历都会清空其结构的所有元素,只不过每次出队或弹栈都会先将队列取顶后再出队。
遍历函数
void Print(MST* obj)
{
assert(obj);
printf("Top-> ");
while (!Empty(obj))
{
printf("%d ", Top(obj));
Pop(obj);
}
printf("<-Bot\n");
}
测试用例1
//栈的队列指针初始化
MST* MyST = StackCreate();
//队列压栈
Push(MyST, 1);
Push(MyST, 2);
Push(MyST, 3);
Push(MyST, 4);
//遍历打印和全部弹栈
Print(MyST);
printf("栈顶元素为:%d\n", Top(MyST));
printf("栈中有%d个元素\n", Size(MyST));
printf("栈是否为空:%d\n", Empty(MyST));
结果观察
Top-> 4 3 2 1 <-Bot
栈顶元素为:-1
栈中有0个元素
栈是否为空:1
测试用例2
MST* MyST = StackCreate();
Push(MyST, 1);
Push(MyST, 2);
Push(MyST, 3);
Push(MyST, 4);
printf("栈顶元素为:%d\n", Top(MyST));
Pop(MyST);
Pop(MyST);
printf("栈顶元素为:%d\n", Top(MyST));
Push(MyST, 5);
Push(MyST, 6);
Push(MyST, 7);
printf("栈顶元素为:%d\n", Top(MyST));
Pop(MyST);
Push(MyST, 8);
Push(MyST, 9);
printf("栈中有%d个元素\n", Size(MyST));
printf("栈是否为空:%d\n", Empty(MyST));
Print(MyST);
printf("栈顶元素为:%d\n", Top(MyST));
printf("栈是否为空:%d\n", Empty(MyST));
结果观察
栈顶元素为:4
栈顶元素为:2
栈顶元素为:7
栈中有6个元素
栈是否为空:0
Top-> 9 8 6 5 2 1 <-Bot
栈顶元素为:-1
栈是否为空:1
测试用例3
MST* MyST = StackCreate();
Push(MyST, 1);
Push(MyST, 2);
Push(MyST, 3);
Push(MyST, 4);
MyST = Free(MyST);
Print(MyST);
结果观察
当函数执行到Free释放时,可看到实参和旗下指针已经被全部释放:
所以当空指针进入Print打印遍历函数时,就会被指针断言判空所截断,程序终止。
为空:%d\n", Empty(MyST));
结果观察
```c
栈顶元素为:4
栈顶元素为:2
栈顶元素为:7
栈中有6个元素
栈是否为空:0
Top-> 9 8 6 5 2 1 <-Bot
栈顶元素为:-1
栈是否为空:1
测试用例3
MST* MyST = StackCreate();
Push(MyST, 1);
Push(MyST, 2);
Push(MyST, 3);
Push(MyST, 4);
MyST = Free(MyST);
Print(MyST);
结果观察
当函数执行到Free释放时,可看到实参和旗下指针已经被全部释放:
所以当空指针进入Print打印遍历函数时,就会被指针断言判空所截断,程序终止。