【C语言】队列与栈的相互实现和循环队列的设计

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

本文选择题目的链接(均来源于leetcode):
用栈实现队列
用队列实现栈
设计循环队列


一、用栈实现队列

1.栈的简介

栈的特点是LIFO,即先进后出,删除元素和增添元素的操作只在一端进行,我们进行操作的位置被称为栈顶

在这里插入图片描述
在“栈”中增加元素的过程一般被称为“压栈”,删除元素的操作被称为“出栈”;
如果把栈类比为步枪的单价、元素比作子弹的话,那么“压栈”就是在弹夹里放入子弹,“出栈”则是把弹夹中的子弹打出;

一般用数组实现栈,这种栈的结构和顺序表很像

还不太熟悉栈的读者可以先了解一下后文的思路,有兴趣的话可以看一下下面的函数代码:

typedef int STDatatype;
typedef struct Stack
{
	STDatatype* a;
	int capacity;
	int top;   
}ST;
void StackInit(ST* ps)
{
	ps->a = (STDatatype*)malloc(sizeof(STDatatype) * 4);
	if (ps->a == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	ps->top = 0;
	ps->capacity = 4;
}

void StackDestroy(ST* ps)
{
	free(ps->a);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}

void StackPush(ST* ps, STDatatype x)
{
	if (ps->top == ps->capacity)
	{
		STDatatype* tmp = (STDatatype*)realloc(ps->a, ps->capacity * 2 * sizeof(STDatatype));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}

		ps->a = tmp;
		ps->capacity *= 2;
	}

	ps->a[ps->top] = x;
	ps->top++;
}
void StackPop(ST* ps)
{
	ps->top--;
}

STDatatype StackTop(ST* ps)
{
	return ps->a[ps->top - 1];
}

bool StackEmpty(ST* ps)
{
	assert(ps);
	return ps->top == 0;
}

int StackSize(ST* ps)
{
	assert(ps);

	return ps->top;
}

2.实现队列的思路

题目链接:用栈实现队列

题目要求用两个栈实现队列,但并没有直接提供给我们栈的函数代码,所以首先要把前文的函数代码粘贴进去/doge
队列也因此要这样写:

typedef struct {
	ST spush;//不可能期望只创建一个指针 指针就给你把指向的空间也创建好,所以这里不能用指针
	ST spop;
} MyQueue;
MyQueue* myQueueCreate() {
	MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));
	StackInit(&obj->spush);
	StackInit(&obj->spop);
	return obj;
}

重点是出/入队列的函数改造怎么写
假设我先在一个栈中依次插入1,2,3,4:
在这里插入图片描述
这时如果我要删除的话,按理来说因为我实现的是队列,所以“出队列”的元素应该是1,但正常出栈的话将会删除4,这时就需要另一个栈参与进来了:
首先存储这个栈的栈顶元素并插入进另一个队列当中,然后将栈顶元素删除:
在这里插入图片描述
再重复3次上述操作:
在这里插入图片描述
然后再对另一个元素进行出栈操作,就可以成功删除先进入“队列”的元素了,但是如果想插入新元素的话,只需要在左侧的栈插入元素就可以了

左侧的栈专门用于插入元素,代码中用spush表示;右侧的栈专门用于删除元素,,用spop表示
如果在进行“删除/插入”元素的过程中发现当前队列空了,那么就将另一栈的全部元素转移到当前队列当中

所以入队列的函数相对比较简单:

void myQueuePush(MyQueue* obj, int x) {
	StackPush(&obj->spush, x);
}

考虑到有一个名为myQueuePeek的函数功能为返回队列开头的元素,我们可以先写出这个函数,把“转移元素”的操作写道这个函数里面,然后在出队列函数中进行复用:

int myQueuePeek(MyQueue* obj) {
	if (StackEmpty(&obj->spop))//spop为空时,将spush的元素转移到spop中
	{
		while (!StackEmpty(&obj->spush))
		{
			StackPush(&obj->spop, StackTop(&obj->spush));
			StackPop(&obj->spush);
		}
	}
	return StackTop(&obj->spop);
}

出队列函数就可以简洁一点:

int myQueuePop(MyQueue* obj) {
	int ret = 0;
	ret = myQueuePeek(obj);
	StackPop(&obj->spop);
	return ret;//题目要求返回已删除的元素
}

3.其他函数代码

就剩两个函数了:

bool myQueueEmpty(MyQueue* obj) {

	return (StackEmpty(&obj->spush) && StackEmpty(&obj->spop));
}//两个栈都为空时,队列为空

void myQueueFree(MyQueue* obj) {
	StackDestroy(&obj->spush);
	StackDestroy(&obj->spop);
	free(obj);
}//依次释放两个栈以后,释放队列obj

二、用队列实现栈

1.队列的简介

和栈的“先进先出”不同,队列的特点是“先进先出”
在这里插入图片描述
其中插入元素的一段被称为“队尾”,删除元素的一段被称为“队头”,由于在使用数组时“出队列”的效率会低一点,所以队列一般用链表实现;

单链表的讲解可以看一下我的这篇博客,全文9000多字(包括代码),希望大家能救一下这个位数的阅读量
传送门

函数代码:

void QueueInit(Queue* pq)
{
	assert(pq);
	pq->head = NULL;
	pq->tail = NULL;
	pq->size = 0;
}

void QueueDestroy(Queue* pq)
{
	assert(pq);
	QNode* cur = pq->head;
	while (cur)
	{
		QNode* del = cur;
		cur = cur->next;
		free(del);
	}
	pq->head = pq->tail = NULL;
	pq->size = 0;
}

void QueuePush(Queue* pq, QDataType x)
{
	assert(pq);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;

	if (pq->tail == NULL)
	{
		pq->head = pq->tail = newnode;
	}
	else
	{
		pq->tail->next = newnode;
		pq->tail = newnode;
	}
	pq->size++;
}

void QueuePop(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));

	if (pq->head->next == NULL)
	{
		free(pq->head);
		pq->head = pq->tail = NULL;
	}
	else
	{
		QNode* del = pq->head;
		pq->head = pq->head->next;
		free(del);
	}

	pq->size--;
}

QDataType QueueFront(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->head->data;
}

QDataType QueueBack(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->tail->data;
}

bool QueueEmpty(Queue* pq)
{
	assert(pq);

	return pq->head == NULL && pq->tail == NULL;
}

int QueueSize(Queue* pq)
{
	assert(pq);
	return pq->size;
}

2.实现栈的思路

题目链接:用队列实现栈
题目要求使用两个队列,故技重施:

typedef struct 
{
    Queue p1;
    Queue p2;
} MyStack;

先向其中一个队列依次插入元素1、2、3:
在这里插入图片描述
看起来一切正常,但如果想像栈一样删除元素3的话,也要把p1中的前几个元素全部转移到p2中,p1中剩余的最后一个元素直接删除,不进入p2:

注意队列是从队头删除,从队尾插入,所以转移到p2以后元素顺序不变
在这里插入图片描述
插入元素时,直接插入有元素的那个队列;删除元素时,先把现有队列除最后一个元素M以外的所有元素转移到另一个队列,然后删除元素M

体感上要比上一题的过程简单一点,代码:

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

int myStackPop(MyStack* obj) 
{
    int ret=0;
    if(!QueueEmpty(&obj->p1))
    {
        while(QueueSize(&obj->p1)!=1)
        {
            QueuePush(&obj->p2,QueueFront(&obj->p1));            
            QueuePop(&obj->p1);
        }
        ret=QueueFront(&obj->p1);
        QueuePop(&obj->p1);
    }
    else
    {
        while(QueueSize(&obj->p2)!=1)
        {
            QueuePush(&obj->p1,QueueFront(&obj->p2));            
            QueuePop(&obj->p2);
        }
        ret=QueueFront(&obj->p2);
        QueuePop(&obj->p2);
    }
    return ret;
}

这次就不复用函数“StackTop”,感兴趣的话建议自己试一下

int myStackTop(MyStack* obj)
 {  
    int ret=0;
    if(!QueueEmpty(&obj->p1))
    {
        while(QueueSize(&obj->p1)!=1)
        {
            QueuePush(&obj->p2,QueueFront(&obj->p1));            
            QueuePop(&obj->p1);
        }
        ret=QueueFront(&obj->p1);
        QueuePush(&obj->p2,QueueFront(&obj->p1));  
        QueuePop(&obj->p1);
    }
    else
    {
        while(QueueSize(&obj->p2)!=1)
        {
            QueuePush(&obj->p1,QueueFront(&obj->p2));            
            QueuePop(&obj->p2);
        }
        ret=QueueFront(&obj->p2);
        QueuePush(&obj->p1,QueueFront(&obj->p2));  
        QueuePop(&obj->p2);
    }
    return ret;
}

3.其他函数代码

和上一题的函数长得“一模一样”

bool myStackEmpty(MyStack* obj) {
    return (QueueEmpty(&obj->p1)&&QueueEmpty(&obj->p2));
}

void myStackFree(MyStack* obj) {
    QueueDestroy(&obj->p1);
    QueueDestroy(&obj->p2);
    free(obj);
}

三、设计循环队列

题目链接:设计循环队列
“循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。”
这里我们需要设置两个指针:

在这里插入图片描述
其中下标为4的空间不存储元素

在插入元素时,先存储到下标为rear的部分中,然后rear向后移动一位;
删除元素时front向后移动一位

有一种方法是用环形链表做,但是这种方法不容易获取队尾元素,但可以通过增加新变量提前存储rear的前一个值来解决

除了直接写一个唤醒链表以外,我们还可以手动控制指针走到队列尽头时回到起点,达到类似于“环形”的效果,当然这种写法需要格外注意边界条件;

既然只是一个直线型的结构的话,也没必要坚持用链表写了,毕竟数组在处理数据的时候要更灵活一些;

唤醒列表的结构:

ypedef struct {
    int *a;
    int front;
    int rear;
    int k;
} MyCircularQueue;

题目要求创建一个长度为k的环形队列,按照我们前面的推理,一共需要创建(k+1)份空间:

MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    obj->k=k;
    obj->a=(int*)malloc(sizeof(int)*(k+1));
    obj->front=obj->rear=0;
    return obj;
}

入队列的过程很简单:首先判断队列是否为空,然后将a[rear]置为想要的值,然后rear++,但边界条件不太好想:
在这里插入图片描述
如果重复上面的操作,当rear走到队列尽头时,rear的值增加到5;
在这时我们需要把它置为0,也就是在rear++之后将rear%=(k+1),这样就可以保证rear在下标范围内变化时不变,超过时则返回起点,实现循环;

bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    if(myCircularQueueIsFull(obj))
        return false;
    obj->a[obj->rear++]=value;
    obj->rear%=(obj->k+1);
    return true;
}

在进行出队列操作时,front向后移动时也要进行同样的操作:

bool myCircularQueueDeQueue(MyCircularQueue* obj) {
   if(myCircularQueueIsEmpty(obj))
        return false;
    obj->front++;
    obj->front%=(obj->k+1);
    return true;
} 

当rear等于front时,队列为空:

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return (obj->rear==obj->front);
}

当“(rear+k)%(k+1)等于front”时,队列为满,函数代码:

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return ( (obj->rear+1)%(obj->k+1)==obj->front);
}

返回队头队尾的函数非常简单,如果用循环链表写的话会非常麻烦:

int myCircularQueueFront(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
        return -1;
    return obj->a[obj->front];
}

int myCircularQueueRear(MyCircularQueue* obj) {
     if(myCircularQueueIsEmpty(obj))
        return -1;
    return obj->a[(obj->rear+obj->k)%(obj->k+1)];
}
void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->a);
    free(obj);
}

这里需要注意,如果你按顺序把上面的函数粘贴到题目里面,一定会出现报错的,原因是在前面的函数中我们就使用了“ 判空”和“判满”两个函数,但这两个函数却位于“增删”函数之后,记得把“ 判空”和“判满”两个函数剪切到最前面就好了


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alexanderite

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值