栈和队列OJ练习——栈实现队列,队列实现栈

⭐栈实现队列

栈与队列的数据存储方式完全不同,栈的数据遵循先进后出模式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;
  1. 定义包含指向两个栈空间的指针Push和Pop,前者用于临时存放压入的待出栈数据,后者用于出栈,注意两个栈都需要初始化,且占用的内存空间均不相同,指针包含在结构体内只是为了便于管理和解引用。

🎀栈指针结构体初始化函数

MQ* StructInit()									//初始化栈指针
{
	MQ* Stacks = (MQ*)malloc(sizeof(MQ));
	Stacks->Push = StackInit();
	Stacks->Pop = StackInit();
	return Stacks;
}
  1. 栈的初始化函数StackInit包含了栈结构体信息的内存空间开辟,栈指针和数据的置空。因为该算法涉及两个栈空间的开辟和使用,初始化时也需要将两个栈指针指向的栈空间都初始化,以便后续其他函数的使用。
🌠队列“压栈”

在队列一章我们曾使用链表的方式实现队列数据的入队,而使用顺序表的方式同样可是实现入队操作,此处因为需使用栈的空间结构实现队列功能,实质上也使用了顺序表的方式入队。对于队列的“压栈”操作,只需根据需求将需要入队的数据全部压到Push栈中即可,原理图如下:

🎀队列压栈函数

void Push(MQ* obj, STEtype x)
{
	assert(obj);
	StackPush(obj->Push, x);		//调用了栈自身的压栈函数
}
🌠栈的“出队”

使用栈入队与压栈的方式大体相同,因为不需要对数据进行过多的操作,直接将数据存入即可,如果此时直接从Push栈里将数据弹出,就是栈的出栈方式。但如果想要模拟队列的出队方式,需要对栈的弹出做些文章,原理图如下:

image-20220629170912882

  1. 对Push栈压入部分数据(即入队操作)后,执行出队命令,此时不能将Push栈的数据直接弹出,而应该全部倒入Pop栈中,具体倒入的方式为将Push中的数据逐个取顶后,将该数压入Pop栈,则最后进入Push栈中的数据挪到了Pop底部,而最先压入Push占中的数据则挪到了Pop顶部(Push栈顶->Pop栈底,Push栈底->Pop栈顶),也即逆序了Push栈中的数据,逆序完成后从Pop栈出队,即为与压入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栈顶元素弹出
}
  1. 观察代码,可以发现数据从Push栈倒入Pop栈过程仅有在Pop栈为空的情况下才能执行,这是因为先压入Push栈的数据应该先从Pop出栈,而后入的数据则只能跟在未完成出栈的数据后面。如果Pop栈中的数据仍未出完,此时就将Push栈中的数据倒过去,则仍未出队的更早的数据就会更晚出队,不符合队列性质,可由如下原理图解释:
    在这里插入图片描述

🎃一定要记住,要符合栈的队列出队性质,数据的入队只能从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));

观察结果

image-20220629222004143
🌠遍历双栈“队列”

对于栈和队列的结构已知,如果要对这两种特殊的线性结构进行数值遍历,则需要清空栈或队列的元素,将元素全部弹栈或出队,这样遍历完成后栈或队列均为空。对于使用栈结构模拟的队列亦是如此。

🎀遍历函数

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);
}

释放双栈构成的队列时,需要将先前开辟过的所有内存空间均释放,这些空间包括:

  1. Push栈和Pop栈初始化开辟和扩容的数组空间arr
  2. 指向双栈的指针Push和Pop
  3. 包含双栈指针的结构体信息指针obj

🎀双栈的队列释放函数

MQ* Free(MQ* obj)								//销毁队列
{
	assert(obj);								//检查双栈指针有效性
    obj->Push = StackDestroy(obj->Push);		//分别释放两个栈,顺序可以颠倒
    obj->Pop = StackDestroy(obj->Pop);

	free(obj);									//再释放双栈结构体信息指针,置空后返回
	obj = NULL;
	return obj;
}

🌈调试观察结果

image-20220630104136175


⭐队列实现栈

在前一章中,我们使用了链表的方式实现了队列的基本结构,而使用队列的结构来实现栈,其思维逻辑与栈实现队列的大体相同,都是需要进行数据的倒入和交换。因为需要使用到队列的结构,所以队列的基本函数接口也可以参考前章中如上文的栈和队列链接,而本课题在此基础上实现对栈的数据存储和访问先进后出(FILO)的模拟。

🥝双队列结点迁移法

使用双栈的队列模拟实现,使用了两个栈Push和Pop栈分别实现入队与数据倒入Pop栈再取顶弹出的方式实现出队操作。而要使用队列模拟栈的数据存储和访问方式,也需要使用到两个队列,分别进行结点数据的入队和出队,整体思路大致如下图所示:

队列实现栈1

🎃可以看出,双队列的“压栈”操作与队列的入队几乎没有区别,都是将数据直接入队“压栈”即可,而在“弹栈”过程中,队列将数据的出队为了满足栈的性质,即最先存储的数据最后出队,而最后存储的数据反而最先出队,所以需要将队列最后入队的那个数据出队即可。但根据队列的性质,出队操作仅能对队头元素弹出,所以将一个非空队列中最后一个结点元素前面的所有结点都依次入队到另一个队列,再将最后一个元素出队,即可满足要求。

🎀双队列结构体模拟栈

typedef int QEtype;
typedef struct Queue						//基于链表结构的队列结构体定义
{
	QEtype data;
	struct Queue* next;
}QE;

//指向两个队列指针的指针
typedef struct MyStack
{
	QE* Q1;									//指向第一个队列
	QE* Q2;									//指向第二个队列
}MST;
  1. 因为要同时控制两个队列的操作,所以在定义队列结构的基础上,还需再定义指向两个队列的头结点指针方便统一管理。队列因为是以链表为基础定义,所以无需初始化,插入数据时直接开辟新结点入队数据即可。

🎀双队列指针初始化函数

MST* StackCreate()
{
	MST* Queues = (MST*)malloc(sizeof(MST));		//开辟包含队列指针信息的结构体空间初始化
	Queues->Q1 = Queues->Q2 = NULL;					//将两个指向Q1和Q2队列的指针初始化置空
	return Queues;
}
  1. 有了初始化函数,与空间和指针初始化函数相对应的是队列空间和指针的销毁函数。

🎀双队列指针销毁函数

MST* Free(MST* obj)
{
	assert(obj);
	QueueDestroy(&obj->Q1);			//将队列Q1和Q2销毁,即链表中所有结点的释放,并将队列指针置空
	QueueDestroy(&obj->Q2);
	free(obj);						//将指向队列指针信息的结构体形参指针释放并置空,并返回给实参
	obj = NULL;
	return obj;
}
  1. 因为使用的是两个队列对栈的模拟实现,所以如果对该栈结构判空。

🎀双队列判空函数:

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);								//最后将仅留下一个结点的原非空队末元素出队,同时此队列变为空队列
}
  1. 如果对一个由双队列构成的栈结构进行弹栈,通过对两个队列的头结点地址进行判空检测,如果均为空则拒绝执行弹栈,双队列判空函数Empty()已由上述给出。
  2. 如果该栈不为空,先假设Q1为空队列,Q2为非空队列,为了验证假设是否正确,需要对被假设为空队列的Q1进行链表判空检测,如果验证Q1为空则不需要更改,否则需要与Q2交换头衔便于后续操作。此处需要定义两个指向队列指针的二级指针Empty和NonEmpty,用于规定和矫正Q1与Q2的空与非空性质。因为可能需要更改Q1指针为非空或空,所以取Q1和Q2的地址进入判空判断!QueueEmpty(Empty),当原Empty指向的队列指针Q1验证其所在队列为非空时,原假设证伪,需要与Q2指针交换指向,使空指针Empty改指向空队列Q2,而原指向空队列Q2的非空指针NonEmpty则需改指向非空队列Q1,即完成了空与非空二级指针指向空与非空队列一级指针的一一对应。
  3. 再次强调,因为可能需要改变队列的一级指针Q1和Q2的地址,所以必须通过取指针地址的方式更改指针指向,而不能传入队列指针本身以一级形参地址的方式修改(也可通过一级指针修改后通过返回值传回,将新地址赋值于原指针)。
  4. 当指针纠正队列指向后,需要进行的就是对非空队列结点的迁移,除了位于队尾的末节点,其余结点通过函数内循环依次取顶并入队于空队列,每取顶入队一次就出一次队,便于对队列后续数据的顺利访问。当非空队列仅剩队尾最后一个结点时,取顶与入队出队循环结束,将该结点作为栈顶元素单独弹出,便完成了队列的弹栈(原理图已于开头给出)。
  5. 如果数据在入队与出队之间交替,新增的数据也必须遵从值以非空队列的一方入队,并且越晚入队的数据越先弹出,以符合栈的数据结构特征。

原理图如下

在这里插入图片描述

🌠队列取“栈顶”

双队列结构栈的取栈顶元素函数也需要寻找空与非空队列,在队列结构中,非空队列的队尾元素是最后入队的,所以该元素即为栈的栈顶元素,遍历非空队列并对队尾元素取值返回即可。

🎀双队列栈取顶函数

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);

🌈结果观察

image-20220630163815631

当函数执行到Free释放时,可看到实参和旗下指针已经被全部释放:

image-20220630164101146

所以当空指针进入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);

🌈结果观察

image-20220630163815631

当函数执行到Free释放时,可看到实参和旗下指针已经被全部释放:

[外链图片转存中...(img-Hb82IP4F-1661495327497)]

所以当空指针进入Print打印遍历函数时,就会被指针断言判空所截断,程序终止。


⭐后话

  1. 博客项目代码开源,获取地址请点击本链接:栈和队列OJ 云代码仓
  2. 若阅读中存在疑问或不同看法欢迎在博客下方或码云中留下评论。
  3. 本章练习题目来源:队列实现栈栈实现队列
  4. 欢迎访问我的Gitee码云,如果对您有所帮助还可以一键三连,获取更多学习资料请关注我,您的支持是我分享的动力~
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值