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

🏠个人主页:黎雁

🎬作者简介: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题,把理论转化为解题能力!😜


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

相关推荐
知南x3 分钟前
【Ascend C系列课程(高级)】(1) 算子调试+调优
c语言·开发语言
NEXT0615 分钟前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法
2的n次方_2 小时前
Runtime 执行提交机制:NPU 硬件队列的管理与任务原子化下发
c语言·开发语言
凡人叶枫3 小时前
C++中智能指针详解(Linux实战版)| 彻底解决内存泄漏,新手也能吃透
java·linux·c语言·开发语言·c++·嵌入式开发
小妖6663 小时前
js 实现快速排序算法
数据结构·算法·排序算法
凡人叶枫4 小时前
C++中输入、输出和文件操作详解(Linux实战版)| 从基础到项目落地,避坑指南
linux·服务器·c语言·开发语言·c++
傻乐u兔5 小时前
C语言进阶————指针3
c语言·开发语言
独好紫罗兰6 小时前
对python的再认识-基于数据结构进行-a003-列表-排序
开发语言·数据结构·python
wuhen_n6 小时前
JavaScript内置数据结构
开发语言·前端·javascript·数据结构
2401_841495646 小时前
【LeetCode刷题】二叉树的层序遍历
数据结构·python·算法·leetcode·二叉树··队列