从 “排队打饭” 到代码实现:超通俗的队列入门指南

前言:

        日常生活中,"排队"是再熟悉不过的场景——无论是食堂打饭、超市结账、医院挂号,还是小程序等餐,都遵循着"先到先得"的基本原则。在计算机科学中,队列(Queue)是对"排队逻辑"的抽象实现,作为遵循「先进先出(First In First Out, FIFO)」原则的线性数据结构,它已成为算法和工程领域最常用的基础数据结构之一。

本文系统讲解队列这一数据结构,从基础概念到实际应用进行全面剖析。通过清晰的逻辑和丰富的实例,即使是数据结构初学者也能轻松掌握核心要点。

        

一、队列的核心概念

        

1.1队列的定义

        队列是一种特殊的线性表其只允许在一端进行插入数据操作,在另一端进行删除数据操作,队列具有先进先出 FIFO(First In First Out)。

        

1.2队列的基础知识

        为了方便理解,先要明确队列的基础知识

front 指针:始终指向 “最早入队的元素”(队头),出队时仅移动 front,保证先入队的元素先被移除 。

        

tail指针:始终指向“下一个入队元素的位置(队尾的后一位)”,入队时仅移动tail,保证新元素只能从队尾加入。

        

出队:始终删除链表头部(front 指向的节点),而头部节点正是 “最早入队的元素”。

        

入队和出队相结合,完美契合了FIFO,先入的元素先出,后入的元素后出。

        

1.3进队出队的示意图    

        

入队(EnQueue):新元素始终接在链表尾部(tail指向的节点),保证进入顺序与链表节点顺序一致。

出队(DeQueue):始终删除链表头部(front 指向的节点),而头部节点正是 “最早入队的元素”。

        

入队和出队相结合,完美契合了FIFO,先入的元素先出,后入的元素后出。

        

二、队列的实现

        本文通过使用链表为底层数据结构来进行实现队列

如下图所示:通过对单向链表进行头删和尾删的方式进行模拟队列出队和入队。

        

2.1队列实现的相关接口

2.2队列实现的相关文件

        

        

2.3Queue.h文件

        Queue.h头文件实现:通过结构体定义队列,声明队列的相关接口函数。

#pragma once

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

typedef int QDataType;

//创建链式节点
typedef struct QListNode
{
	struct QListNode* next;
	QDataType val;

}QNode;


//定义一个结构体,成员为头节点指针和尾节点指针
typedef struct Queue
{
	QNode* phead;
	QNode* ptail;

	//用于记录队列的元素个数
	int size;
	
}Queue;


//队列初始化
void QueueInit(Queue * q);

//队尾插入
void QueuePush(Queue* q, QDataType x);

//队头删除
void QueuePop(Queue* q);

//获取队头元素
QDataType QueueFront(Queue *q);

//获取队尾元素
QDataType QueueBack(Queue *q);

//获取队列有效元素个数
int QueueSize(Queue *q);

//判断队列是否为空
bool QueueEmpty(Queue *q);

//销毁队列
void QueueDestroy(Queue *q);

        

对于Queue.h文件主要关注如下代码:

typedef int QDataType;

//创建链式节点
typedef struct QListNode
{
	struct QListNode* next;
	QDataType val;

}QNode;


//定义一个结构体,成员为头节点指针和尾节点指针
typedef struct Queue
{
    //队列头节点指针
	QNode* phead;
    
    //队列尾节点指针
	QNode* ptail;

	//用于记录队列的元素个数
	int size;
	
}Queue;

1.struct QListNode  创建链表节点的结构体

        

成员一:struct QListNode* next;  用于指向下一个节点的指针        

        

成员二:储存在节点中的值,通过typedef int QDataType ,方便后续数据类型的替换

        

typedef struct QListNode QNode     重命名结构体

    

    

2. struct Queue  维护队列指针的结构体

        

成员一:队列头节点指针 QNode* phead;
    

成员二:队列尾节点指针 QNode* ptail;

        

成员三:队列中的元素个数  size

        

思考1:为什么要定义一个结构体专门用于维护队列指针?

        

如果队列初始时为空,以及队尾插入元素需要改变头节点指针和尾节点指针,需要使用二级指针过于麻烦

对于函数接口的调用过于复杂,不利于函数的使用。

        

如下函数为例:
void QueuePush(QNode **phead , QNode **pptail ,QDataType x);

                

而通过队列指针结构体维护就可以更加方便:

void QueuePush(Queue* q, QDataType x);

        

思考2:为什么这里只需要传入Queue* ,这个结构体的一级指针,而上述需要传入QNode **,这个结构体的二级指针呢?

        

对于需要传入QNode **这个指针:

        

举个典型场景:向空队列插入第一个节点

        
初始状态:QNode *phead = NULL,*ptail = NULL(队列为空);

        
插入节点后:需要让phead 和 ptail 都指向新节点(phead = newNode,ptail = newNode)。

        
此时,函数内部需要修改外部的phead和ptail(指针本身的指向)。

        

由于函数参数是值传递:

        
若传入QNode* phead(一级指针),函数内修改的是phead的副本,外部的原phead仍为NULL(无效);

        
必须传入QNode**phead(二级指针,即 “指针的指针”),才能通过*phead = newNode修改外部phead的指向。

        
简言之:当需要修改 “指针变量本身” 时,必须用二级指针。       

        


   

对于只需要传入Queue* :

        

因为入队操作只需要修改front的指向 和 tail的指向 ,而不是修改这个Queue* q这个一级指针指向,才需要使用Queue** pq。

    

还是以 “向空队列插入第一个节点” 为例:

        
初始状态:q->front = NULL,q->tail = NULL(结构体成员为空);

        
插入节点后:需要修改q->front = newNode,q->tail = newNode(修改结构体内部的指针成员)。

        
由于函数参数是Queue* q(一级指针):

             
通过q可以直接访问结构体成员(q->front),并修改成员的指向(q->front = newNode);

这里不需要修改q本身的指向(q始终指向同一个队列结构体),因此一级指针足够。

        

简言之:当只需要修改 “结构体内部的成员”(即使成员是指针),一级结构体指针足够。

        

思考3:为什么单链表的实现不使用头节点指针和尾节点指针方式实现?

        
因为尾删时需要找到尾节点前一个节点,定义尾节点意义不大,而对于队列而言只能从队列的头部进行删除,从队列的尾部进行插入。

        

2.4Queue.c文件

        搞定了上面三个思考问题,那么我们就可以轻松+愉快的实现队列了!

①队列初始化

void QueueInit(Queue* q)
{
	assert(q);

	//队列的头指针和尾指针都为空
	q->phead = q->ptail = NULL;
	q->size = 0;
}

        

②入队

//入队
void QueuePush(Queue* q, QDataType x)
{
	assert(q);
	QNode* newnode = (QNode *)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc fail !");
		return;
	}
	newnode->val = x;
	newnode->next = NULL;

	//队头指针为空
	if (q->phead == NULL)
	{
		q->phead = q->ptail = newnode;
	}
	else
	{
		//队尾插入节点
		q->ptail->next = newnode;

		//更新队尾指针位置
		q->ptail = q->ptail->next;
	}

	//更新队列元素个数
	q->size++;
}

温馨提示:

        当队列为空时,需要进行入队,则需要同时改变队头指针和队尾指针,使其指向新节点。

        

③出队

//出队
void QueuePop(Queue* q)
{
	assert(q);
	//判断是否能够进行删除
	assert(q->size > 0);

	//只有一个节点时
	if (q->phead->next == NULL)
	{
		free(q->phead);
		q->phead = q->ptail = NULL;
	}
	else
	{
		//保存头节点下一个节点
		QNode* tmp = q->phead->next;

		//释放当前头节点
		free(q->phead);

		//更新头节点位置
		q->phead =tmp;
	}

	//更新队列元素个数
	q->size--;
}

        

温馨提示:

        当队列只有一个节点时,且需要进行出队,需要进行同时改变队头指针和队尾指针,否则在该节点释放后,队尾指针成为了野指针。

        

如下图所示:

        

④获取队头元素

//获取队头元素
QDataType QueueFront(Queue* q)
{
	assert(q);
	assert(q->size > 0);

	return q->phead->val;
}

        

⑤获取队尾元素

//获取队尾元素
QDataType QueueBack(Queue* q)
{
	assert(q);
	assert(q->size > 0);

	return q->ptail->val;
}

        

⑥获取队列的有效元素个数

//获取队列的有效元素个数
int QueueSize(Queue* q)
{
	assert(q);
	return q->size;
}

        

⑦判断队列是否为空

//判断队列是否为空
bool QueueEmpty(Queue* q)
{
	assert(q);
	
	return q->size == 0;
}

        

⑧销毁队列

//销毁队列
void QueueDestroy(Queue* q)
{
	assert(q);
	QNode* pcur = q->phead;
	while (pcur)
	{
		QNode* tmp = pcur->next;
		free(pcur);
		pcur = tmp;
	}

	q->phead = q->ptail = NULL;
	q->size = 0;
}

        

2.5Test.c文件

#include"Queue.h"

// 测试基本入队、出队及元素获取
void TestQueueBasic()
{
    Queue q;
    QueueInit(&q);

    // 测试空队列初始状态
    printf("=== 测试空队列初始状态 ===\n");
    printf("队列是否为空:%s\n", QueueEmpty(&q) ? "是" : "否");
    printf("初始元素个数:%d\n", QueueSize(&q));
    printf("\n");

    // 测试入队操作
    printf("=== 测试入队操作 ===\n");
    QueuePush(&q, 10);
    QueuePush(&q, 20);
    QueuePush(&q, 30);
    QueuePush(&q, 40);
    printf("入队4个元素后,元素个数:%d\n", QueueSize(&q));
    printf("当前队头元素:%d(预期:10)\n", QueueFront(&q));
    printf("当前队尾元素:%d(预期:40)\n", QueueBack(&q));
    printf("\n");

    // 测试单次出队
    printf("=== 测试单次出队 ===\n");
    QueuePop(&q);
    printf("出队1个元素后,元素个数:%d\n", QueueSize(&q));
    printf("当前队头元素:%d(预期:20)\n", QueueFront(&q));
    printf("当前队尾元素:%d(预期:40)\n", QueueBack(&q));
    printf("\n");

    // 测试多次出队至仅剩一个元素
    printf("=== 测试多次出队至仅剩一个 ===\n");
    QueuePop(&q);
    QueuePop(&q);
    printf("再出队2个元素后,元素个数:%d\n", QueueSize(&q));
    printf("当前队头元素:%d(预期:40)\n", QueueFront(&q));
    printf("当前队尾元素:%d(预期:40)\n", QueueBack(&q));
    printf("\n");

    // 测试出队至空队列
    printf("=== 测试出队至空队列 ===\n");
    QueuePop(&q);
    printf("全部出队后,元素个数:%d\n", QueueSize(&q));
    printf("队列是否为空:%s\n", QueueEmpty(&q) ? "是" : "否");
    printf("\n");

    // 测试空队列入队新元素
    printf("=== 测试空队列重新入队 ===\n");
    QueuePush(&q, 50);
    QueuePush(&q, 60);
    printf("重新入队2个元素后,元素个数:%d\n", QueueSize(&q));
    printf("当前队头元素:%d(预期:50)\n", QueueFront(&q));
    printf("当前队尾元素:%d(预期:60)\n", QueueBack(&q));

    QueueDestroy(&q);
}

// 测试混合入队出队场景
void TestQueueMixed()
{
    Queue q;
    QueueInit(&q);
    printf("\n\n=== 测试混合入队出队场景 ===\n");

    // 交替入队出队
    QueuePush(&q, 1);
    QueuePush(&q, 2);
    QueuePop(&q);
    QueuePush(&q, 3);
    QueuePop(&q);
    QueuePush(&q, 4);
    QueuePush(&q, 5);
    QueuePop(&q);

    printf("混合操作后,元素个数:%d(预期:2)\n", QueueSize(&q));
    printf("当前队头元素:%d(预期:4)\n", QueueFront(&q));
    printf("当前队尾元素:%d(预期:5)\n", QueueBack(&q));

    QueueDestroy(&q);
}

int main()
{
    TestQueueBasic();   // 基础功能测试
    TestQueueMixed();   // 混合操作测试
    return 0;
}

        

三、队列与栈的简单对比

        

3.1核心定义对比

对比维度栈(Stack)队列(Queue)
核心定义仅允许在 “一端”(栈顶,Top)进行插入和删除的数据结构仅允许在 “一端”(队尾,Rear)插入、“另一端”(队头,Front)删除的数据结构
核心原则LIFO(Last In First Out,后进先出)FIFO(First In First Out,先进先出)
形象类比叠放的盘子:只能从最顶端放新盘子,也只能从顶端拿盘子排队买票:先到的人先买票,后到的人只能排到队尾

        

3.2关键操作与特性对比

对比维度栈(Stack)队列(Queue)
核心操作名称

- 插入:Push(向栈顶添加元素)

- 删除:Pop(从栈顶移除元素)

- 查看:Peek(查看栈顶元素,不删除)

- 插入:Enqueue(向队尾添加元素)

- 删除:Dequeue(从队头移除元素)

- 查看:Peek/Front(查看队头元素,不删除)

数据存取限制仅允许在栈顶进行插入 / 删除,不支持随机访问(无法直接访问栈中间元素)仅允许在队尾插入、队头删除,不支持随机访问(无法直接访问队列中间元素)
常用操作时间复杂度

Push/Pop:O (1)(无需遍历,直接操作栈顶)

- peek查找指定元素:O (n)(需从栈顶依次遍历)

Enqueue/Dequeue:O (1)(直接操作队尾 / 队头)

- peek查找指定元素:O (n)(需从队头依次遍历)

物理实现方式

可基于数组或链表实现:

- 数组栈:需注意 “栈满” 扩容

- 链表栈:无固定大小,灵活扩容

可基于数组或链表实现:

- 数组队列:易出现 “假溢出”(需用循环队列优化)

- 链表队列:无固定大小,灵活扩容(通常用 “带头尾指针的链表” 实现)

        

既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。

评论 41
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值