新手向C语言实现特殊数据结构——队列(含用两个队列实现栈)

本文详细介绍了如何使用单向无头不循环链表实现队列,包括易错接口如出队、入队的详细解析,以及简单接口如初始化、计数、销毁等的实现。此外,还探讨了用两个队列实现栈的方法,以及循环队列的算法图解和代码实现,涵盖了循环队列的初始化、入队、出队、判断状态等操作。文章最后讨论了循环队列的内存管理和接口调用方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

使用单向无头不循环链表实现队列。

队列:符合先进先出逻辑的特殊数据结构。

不适合用数组来实现,虽然入队便捷但是出队时需要挪动队首后的所有元素,效率略低。

单向不循环链表对于找尾的效率确实低,但是可以在结构体中定义出队首节点和队尾节点,分别管理使得入队和出队的效率都高。所以就需要定义两个结构体,一个管理节点的内容,一个管理整个队列。

建议先跳转到第三部分阅读结构体定义的代码,以便后续便于理解。

关于改变链表头节点的方式:

1、二级指针(传指针解引用改变)

2、返回新头(接受改变值)

3、带哨兵位的头节点(本质上不会改变头节点)

4、结构体包一起(大结构体一级指针改变)

此次选择第四种方式。

可能这时候就会有同学这样问了:为什么在实现单向不循环链表的时候不把结构体包一起呢?这样不就解决了找尾效率低的问题吗?

哎,这位同学就年轻了啊。你这样解决了尾插的问题,但是能解决尾删的问题吗?单向不循环链表中时不能通过一个节点找到上一个节点的。也就是尾删了后还是要找尾···

一、易错接口详解

1.1 出队

出队算法:法一:保存下一个节点为next,再删除。

法二:保存要删除的节点为del,把头结点赋值为新节点,然后free掉del

此次选择法一。

需要注意队列为空时调用此接口会出现非法访问的错误,所以需要提前断言报错。

这里把判断是否为空的功能封装成一个函数,详情请跳转至第二部分查看。

特殊情况:当链表删除掉最后一个节点时,ptail会成为野指针。

所以仅有一个节点时就直接free掉,再将头指针和尾指针都赋值为NULL

正常情况下,出队时记得保留队头节点的下一节点的地址,以便头节点迭代。

void QueuePop(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));
	if (pq->phead->next == NULL)
	{
		free(pq->phead);
		pq->phead = pq->ptail = NULL;
	}
	else
	{
		QueueNode* next = pq->phead->next;
		free(pq->phead);
		pq->phead = next;
	}
}

1.2 入队

入队算法:为新的头节点开辟新空间,并插入有效数据。之后的步骤相当于链表的尾插。

但是需要注意特殊情况:队列中没有节点时需要直接把头尾节点都改为新节点。

void QueuePush(Queue* pq, QDataType x)
{
	assert(pq);
	QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	//新节点初始化结束
	if (pq->ptail == NULL)
	{
		pq->phead = pq->ptail = newnode;
	}
	else
	{
		pq->ptail->next = newnode;
		pq->ptail = newnode;
	}
}

二、 简单接口的实现

2.1 队列的初始化

算法:将头指针和尾指针都赋值为NULL

void QueueInit(Queue* pq)
{
	assert(pq);
	pq->phead = pq->ptail = NULL;
}

2.2 队列节点计数

int QueueSize(Queue* pq)
{
	assert(pq);
	int count = 0;
	QueueNode* cur = pq->phead;
	while (cur)
	{
		count++;
		cur = cur->next;
	}
	return count;
}

关于链表计数的接口,如果调用频繁的话就可以在大结构体Queue中加入一个计数成员,入队或出队时修改。如果调用不频繁,就直接在接口中进行计算。

2.3 队列的销毁

算法:利用创建的新指针cur从头走到尾,走到一个节点就删除一个节点,在删除前还需要保留cur指针的下一个节点地址,以便cur指针迭代。最后还不要忘了将头节点的指针phead和为节点的指针ptail指向NULL

void QueueDestory(Queue* pq)
{
	assert(pq);
	QueueNode* cur = pq->phead;
	while (cur)
	{
		QueueNode* next = cur->next;
		free(cur);
		cur = next;
	}
	pq->phead = pq->ptail = NULL;
}

2.4 判断队列是否为空

bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->phead == NULL && pq->ptail == NULL;
}

2.5 返回队头数据

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

2.6 返回队尾数据

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

三、头文件引用、函数与结构体定义

#include<assert.h>
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>

typedef int QDataType;
typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType data;
}QueueNode;

typedef struct Queue
{
	QueueNode* phead;
	QueueNode* ptail;
}Queue;

void QueueInit(Queue* pq);
void QueueDestory(Queue* pq);
void QueuePush(Queue* pq, QDataType x); 
void QueuePop(Queue* pq);                
int QueueSize(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
bool QueueEmpty(Queue* pq);

四、拾枝杂谈

4.1 队列接口调用方式

首先创建一个大结构体Queue的对象q,再把对象初始化,再入队,打印的时候必须打印一个队首元素就将队首元素进行出队,直至队列为NULL时即可将队中所有元素打印完成,最终销毁整个队列,避免内存泄漏。

void Test2()
{
	Queue q;
	QueueInit(&q);
	QueuePush(&q, 1);
	QueuePush(&q, 2);
	QueuePush(&q, 3);
	QueuePush(&q, 4);
	while (!QueueEmpty(&q))
	{
		printf("%d ", QueueFront(&q));
		QueuePop(&q);
	}
	QueueDestory(&q);
}

4.2 用两个队列实现栈

队列的特点:先进先出

栈的特点:后进先出

在这里插入图片描述

算法总结如下:

1、两个队列一个有数据,另外一个为空。

2、入栈时,向不为空的队列中添加数据。

3、出栈时,将有数据的队列中的size - 1个数据导入到为空的队列,把剩下的一个数据出队——即可实现“出栈”。

由于c语言不能像C++那样使用栈的库函数,所以以下代码利用了上文中已经实现好的接口。

首先是定义一个代表栈的结构体,里面的成员为两个队列。

typedef struct {
    Queue q1;
    Queue q2;
} MyStack;

接下来实现创建栈的接口,需要注意不可在此接口中利用临时变量返回所创建栈的地址,因为临时变量会在函数调用结束时销毁,故利用动态开辟内存函数malloc开辟一个栈结构体的对象,再调用已定义好的队列初始化接口将其内部的两个队列进行初始化。

MyStack* myStackCreate() {
    MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
    QueueInit(&pst->q1);
    QueueInit(&pst->q2);
    return pst;
}

入栈的实现:其本质是入队,但是入队时需要保证向不为空的队列中添加数据,这时候QueueEmpty接口可以帮助快速判断哪一个为空队。

这时候有同学可能会问为啥“入栈”的时候非要往不为空的队列里面加数据呢?

在这里插入图片描述

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

出栈的实现:将非空队列的size - 1个元素都入队到另一个队列中,再删除剩下的一个元素。

int myStackPop(MyStack* obj) {
    Queue* emptyQ = &obj->q1;
    Queue* nonemptyQ = &obj->q2;
    if(!QueueEmpty(&obj->q1))
    {
        emptyQ = &obj->q2;
        nonemptyQ = &obj->q1;
    }
    while(QueueSize(nonemptyQ) > 1)
    {
        QueuePush(emptyQ, QueueFront(nonemptyQ));
        QueuePop(nonemptyQ);
    }
    int top = QueueFront(nonemptyQ);
    QueuePop(nonemptyQ);
    return top;
}

取栈顶元素:相当于返回非空队列的队尾元素,直接调用已经实现好的接口即可。

int myStackTop(MyStack* obj) {
    if(!QueueEmpty(&obj->q1))
    {
        return QueueBack(&obj->q1);
    }
    else
    {
        return QueueBack(&obj->q2);
    }
}

判断栈是否为空:只需要判断两个队列是否同时为空即可。

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

销毁栈:调用已有接口分别将两个队列销毁,再释放掉表示栈的结构体对象的空间。

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

4.3 循环队列

4.3.1 循环队列算法图解

在这里插入图片描述

算法总结:

给出既定的空间数,删除数据时不会删除已有节点的空间,而是让头节点指针向后走,插入数据时,让尾节点向后走。

当头节点和尾节点指向同一位置时,循环队列为空。

当尾节点的下一节点指向头节点时,循环队列为满。

需要开辟比既定空间数多一个单位空间,以便区别循环队列为空和为满的状态。

4.3.2 代码实现
4.3.2.1 循环队列的结构体定义

利用的是匿名结构体,没有一开始就给出结构体名称,而是直接typedef重定义匿名结构体的名字,使用时同样不需要再加上struct了。

利用的数据结构是顺序表。如果想用链表实现,需要用到循环链表,但是在判断头节点和尾节点的关系时不太方便。

front为头节点的下标,rear为尾节点的下标。

k为循环队列的空间大小。

typedef struct {
    int* a;
    int front;
    int rear;
    int k;
} MyCircularQueue;
4.3.2.2 判断循环队列是否为空

当头节点下标和尾节点下标相等时循环队列为空。

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return obj->front == obj->rear;
}
4.3.2.3 判断循环队列是否已满

当尾节点的下一个位置是头节点时,循环队列已满。

但是需要注意的是,rear出现在顺序表末尾时,front在顺序表之首,rear前面的位置实质上是顺序表越界的位置,要使rear+1后的数值仍然能和front的数值有效比对,就将rear+1k+1取余。

如下图可以对代码进行简单验证。

注:黑色数字为队列中数据,蓝色数字为顺序表的下标。

在这里插入图片描述

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return (obj->rear + 1) % (obj->k + 1) == obj->front;
}
4.3.2.4 循环队列的初始化

给上一个参数k,会创建k + 1个数据的空间,以便区分队列的空与满的状态。

MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* q = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    q->a = (int*)malloc(sizeof(int)*(k + 1));
    q->front = 0;
    q->rear = 0;
    q->k = k;
    return q;
}
4.3.2.5 循环队列的入队

首先需要判断队列是否已满,若满就返回false

若没有满则在尾节点处插入数据,再让尾节点指针向后跳。

特别注意:当尾节点指针所指向位置是顺序表末尾时,需要让其重新回到开头,即顺序表下标为0处。

插入完成返回true

bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    if(myCircularQueueIsFull(obj))
        return false;
    obj->a[obj->rear] = value;
    obj->rear++;
    if(obj->rear == obj->k + 1)
        obj->rear = 0;
    return true;
}
4.3.2.6 循环队列的出队

首先需要判断队列是否为空,若为空就返回false

若不为空,则让头指针向前跳一步。

但是需要注意如果头指针已经指向顺序表的尾部了,再往前跳就越界了,所以需要将front的值对k+1取余。

这样如此,就算是frontk+1了,也可以立刻返回头指针的位置。

bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
        return false;
    obj->front++;
    obj->front %= obj->k + 1;  
    return true;
}
4.3.2.7 取队头的值

判断是否为空队后返回头指针所指向的数据即可。

int myCircularQueueFront(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
        return -1;
    return obj->a[obj->front];
}
4.3.2.8 取队尾的值

取队尾的值即为取队尾前一个下标的值,即下标为rear - 1的值。

但需要注意若是rear的下标为0,队尾的值应该是下标为k的值。

int myCircularQueueRear(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
        return -1;
    int prevRear = obj->rear - 1;
    if(0 == obj->rear)
        prevRear = obj->k;
    return obj->a[prevRear];
}
4.3.2.9 删除循环队列

释放循环队列结构体对象中的顺序表,释放对象指针本身。

void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->a);
    obj->a = NULL;
    free(obj);
    obj = NULL;
}
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值