栈与队列之队列入门攻略:从核心概念到链表实现

🏠个人主页:黎雁

🎬作者简介:C/C++/JAVA后端开发学习者

❄️个人专栏:C语言数据结构(C语言)EasyX游戏规划程序人生

✨ 从来绝巘须孤往,万里同尘即玉京

文章目录

栈与队列之队列入门攻略:从核心概念到链表实现✨

你好!欢迎来到线性表系列栈与队列篇的第二篇内容~

在上一篇中,我们吃透了"后进先出"的栈结构,而今天的主角------队列,恰恰是栈的"反向搭档",它遵循"先进先出"原则,在日常开发和算法解题中同样不可或缺,比如消息队列、任务排队、二叉树层序遍历等场景都能看到它的身影。

继栈的数组实现后,我们这篇将聚焦队列的核心知识,从概念到实现,手把手带你搞定这个"排队专用"的线性表!


文章摘要

本文为线性表系列栈与队列篇第二篇入门内容,聚焦队列这一受限线性表。通俗讲解队列"先进先出"的核心特性,对比数组与链表两种实现方式的优劣并选定单向链表为最优方案。通过工程化的头文件+源文件结构,完整实现队列的初始化、销毁、入队、出队等核心操作,详解队头队尾指针的边界处理细节,补充空队列、单节点队列等特殊场景的处理逻辑,零基础吃透队列的底层实现。

阅读时长 :约20分钟
阅读建议

  1. 基础薄弱者:先理解队列"先进先出"特性,再对照代码梳理指针逻辑
  2. 工程开发者:重点关注内存释放、队空/队满判断等健壮性细节
  3. 面试备考者:牢记队列的特性、实现方式及典型应用场景
  4. 查漏补缺者:直接查看出队操作中单节点队列的特殊处理逻辑

一、知识回顾:栈 vs 队列的核心差异

在学习队列之前,我们先对比栈和队列的核心区别,快速建立认知:

数据结构 核心特性 操作端 最优实现方式
后进先出(LIFO) 仅栈顶一端 数组(尾插尾删)
队列 先进先出(FIFO) 队尾入、队头出 链表(头删尾插)

简单总结:栈是"一口进出",队列是"一头进、另一头出"!


二、队列的核心概念:什么是"先进先出"?📚

1. 队列的定义

队列是一种特殊的线性表,只允许在一端插入元素 (队尾),在另一端删除元素 (队头),遵循先进先出(FIFO = First In First Out) 原则。

生活中的队列例子随处可见:

  • 排队买奶茶:先排队的人先买到奶茶 → 完美契合"先进先出"
  • 打印机打印任务:先提交的任务先打印 → 典型的队列应用
  • 消息队列:先产生的消息先被消费 → 工程中最常用的队列场景

2. 队列的关键操作术语

操作名称 说明 操作位置
入队/进队 向队列中添加元素 队尾(唯一入队端)
出队/离队 从队列中删除元素 队头(唯一出队端)
队头 允许删除元素的一端 -
队尾 允许插入元素的一端 -

3. 队列的实现方式对比:数组 vs 链表

队列同样有两种实现方式,我们详细分析优劣,选择最优方案:

实现方式 核心思路 优点 缺点
数组 队尾入队(尾插)、队头出队(头删) 实现简单、随机访问快 队头出队需挪动所有元素,时间复杂度O(N),效率极低
链表(推荐) 单向链表:队尾入队(尾插)、队头出队(头删) (用tail指针记录队尾,避免找尾) ① 入队/出队效率O(1) ② 按需分配内存,无需扩容 需额外维护队头/队尾指针,处理空队列/单节点队列边界

结论:单向链表实现队列是最优选择!

  • 链表头删、尾插的时间复杂度都是O(1),完美匹配队列的操作需求
  • 只需维护head(队头)和tail(队尾)两个指针,即可高效完成所有操作
  • 避免了数组出队时挪动元素的性能损耗

三、队列的工程化实现:链表版队列完整代码💻

我们依旧采用"头文件+源文件"的工程化结构实现队列,保证代码规范性。

1. 头文件定义(queue.h):接口声明

c 复制代码
#pragma once
#include <stdio.h>
#include <stdbool.h>
#include <assert.h>
#include <stdlib.h>

// 定义队列存储的数据类型
typedef int QDataType;

// 队列节点结构体(单向链表节点)
typedef struct QueueNode
{
    struct QueueNode* next; // 指向下一个节点的指针
    QDataType data;         // 节点存储的数据
}QNode;

// 队列结构体(维护队头、队尾指针)
typedef struct Queue
{
    QNode* head; // 队头指针(出队端)
    QNode* tail; // 队尾指针(入队端)
}Queue;

// 队列核心操作接口
void QueueInit(Queue* pq);        // 初始化队列
void QueueDestroy(Queue* pq);     // 销毁队列(释放内存)
void QueuePush(Queue* pq, QDataType x); // 入队(队尾插入)
void QueuePop(Queue* pq);         // 出队(队头删除)
QDataType QueueFront(Queue* pq);  // 获取队头元素
QDataType QueueBack(Queue* pq);   // 获取队尾元素
int QueueSize(Queue* pq);         // 获取队列元素个数
bool QueueEmpty(Queue* pq);       // 判断队列是否为空

2. 源文件实现(queue.c):核心逻辑

c 复制代码
#include "queue.h"

// 初始化队列
void QueueInit(Queue* pq) 
{
    assert(pq); // 确保队列指针不为NULL
    pq->head = NULL; // 初始队头为空
    pq->tail = NULL; // 初始队尾为空
}

// 销毁队列(遍历释放所有节点)
void QueueDestroy(Queue* pq) 
{
    assert(pq);
    QNode* cur = pq->head;
    while (cur) // 逐个释放节点
    {
        QNode* next = cur->next; // 保存下一个节点
        free(cur);
        cur = next;
    }
    // 重置指针,防止野指针
    pq->head = NULL;
    pq->tail = NULL;
}

// 入队(队尾插入)
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 = newnode;
        pq->tail = newnode;
    }
    else // 队列非空,尾插
    {
        pq->tail->next = newnode;
        pq->tail = newnode; // 更新队尾指针
    }
}

// 出队(队头删除)
void QueuePop(Queue* pq) 
{
    assert(pq);
    assert(!QueueEmpty(pq)); // 队空禁止出队

    // 处理单节点队列(删除后队头队尾都为空)
    if (pq->head->next == NULL) 
    {
        free(pq->head);
        pq->head = NULL;
        pq->tail = NULL;
    }
    else // 多节点队列,头删
    {
        QNode* next = pq->head->next;
        free(pq->head);
        pq->head = next; // 更新队头指针
    }
}

// 获取队头元素
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;
}

// 获取队列元素个数
int QueueSize(Queue* pq) 
{
    assert(pq);
    int size = 0;
    QNode* cur = pq->head;
    while (cur) // 遍历统计节点数
    {
        size++;
        cur = cur->next;
    }
    return size;
}

// 判断队列是否为空
bool QueueEmpty(Queue* pq) 
{
    assert(pq);
    return pq->head == NULL; // 队头为空则队列空
}

四、核心易错点解析:边界场景处理⚠️

队列实现中最容易出错的是边界场景,我们重点梳理:

  1. 空队列入队:需同时更新head和tail指针,否则会出现tail为NULL的错误
  2. 单节点队列出队:删除后必须将head和tail都置NULL,否则tail会指向已释放的节点(野指针)
  3. 出队前判空 :必须用assert(!QueueEmpty(pq))防止空队列出队,避免访问NULL指针

五、写在最后

恭喜你!这一篇中你已经掌握了队列的核心知识:

  • 理解了队列"先进先出"的核心特性,对比了和栈的关键差异
  • 选定了链表作为队列的最优实现方式,吃透了头删尾插的逻辑
  • 完成了队列的工程化代码实现,掌握了边界场景的处理技巧

队列和栈是数据结构的"基础搭档",二者结合能解决很多复杂问题:

  • 算法层面:二叉树层序遍历、BFS(广度优先搜索)依赖队列
  • 工程层面:消息队列、任务调度、限流削峰都离不开队列

下一篇,我们将进入实战环节,用栈和队列解决经典OJ题,把理论转化为解题能力!😜


点赞+收藏+关注,跟着系列内容一步步吃透栈和队列!你的支持是我创作的最大动力~👍

相关推荐
为何创造硅基生物7 小时前
C语言 结构体内存对齐规则(通俗易懂版)
c语言·开发语言
仰泳之鹅7 小时前
【C语言】自定义数据类型2——联合体与枚举
c语言·开发语言·算法
jolimark8 小时前
C语言自学攻略:小白入门三步走
c语言·编程入门·学习路线·实践项目·自学攻略
cen__y9 小时前
Linux12(Git01)
linux·运维·服务器·c语言·开发语言·git
社交怪人9 小时前
【算平均分】信息学奥赛一本通C语言解法(题号2071)
c语言·开发语言
卢锡荣10 小时前
单芯通吃,盲插标杆 —— 乐得瑞 LDR6020,Type‑C 全场景互联 “智慧芯”
c语言·开发语言·计算机外设
Mr. zhihao10 小时前
深入解析redis基本数据结构
数据结构·数据库·redis
念何架构之路10 小时前
Go语言加密算法
数据结构·算法·哈希算法
AI科技星10 小时前
《数学公理体系·第三部·数术几何》(2026 年版)
c语言·开发语言·线性代数·算法·矩阵·量子计算·agi
失去的青春---夕阳下的奔跑11 小时前
560. 和为 K 的子数组
数据结构·算法·leetcode