数据结构 | 链式队列

一、基础知识

1. 队列核心特性

队列是数据结构中典型的先进先出(FIFO) 线性表,遵循"先排队、先处理"的核心规则,数据操作出入口严格受限。队列仅支持两种核心基础操作:只能在队尾 执行添加数据的入队操作,只能在队头执行删除数据的出队操作,不允许在链表中间位置进行插入、删除元素,也不支持随机访问任意位置数据。

生活常见场景:食堂排队打饭、车站排队购票、后台消息任务排队处理、业务请求排队响应,均是队列先进先出思想的实际应用。

2. 实现方式对比

队列底层主流分为链式队列顺序循环队列两种实现形式,二者底层依托结构不同,优缺点、适用场景差异明显,可根据业务数据特性灵活选择。

|------|----------|-----------------------------------------------------------|
| 实现类型 | 底层结构 | 核心特点 |
| 链式队列 | 单链表+头尾指针 | 动态内存分配,无固定容量限制,不存在假溢出问题,数据增减灵活,适合数据量不确定、频繁增减元素的动态场景 |
| 循环队列 | 数组模拟实现 | 容量固定不可扩容,普通顺序队列存在假溢出缺陷,必须改造为循环队列优化,适合数据量固定、元素增减频率低的稳定业务场景 |

本文重点讲解与C++代码实现:链式队列,下篇博客专门讲解顺序循环队列原理、优化逻辑与完整代码实现,方便大家对比两种队列的优劣与适用场景。

二、结构体设计

链式队列依托动态结点串联的方式存储所有有效数据,整个队列由无数个相同类型的基础结点拼接而成,结点是链式队列最小且唯一的存储单元。结点结构体的设计合理性,直接决定链式队列能否正常串联、数据能否正常存取,是整个链式队列结构的底层核心。

1. 结点结构体

cpp 复制代码
typedef int ELEMETYPE;

// 有效节点设计
struct LQNode {
    ELEMETYPE data;
    LQNode* next;
};

2. 设计原理

ELEMETYPE data 数据域:是结点的核心业务存储成员,专门用于存放队列需要管理、处理的有效数据,所有入队、出队、取值操作本质都是对数据域的读写操作。若无数据域,结点无任何存储意义,队列也就失去了数据管理的核心作用,统一typedef定义类型,后期可快速适配char、string等不同数据需求。

LQNode* next 指针域:是链式结构的连接核心,专门用于指向队列逻辑上的下一个结点。链式队列没有数组下标辅助定位,全靠每个结点的next指针串联所有独立结点,形成有序的队列链式结构。若无指针域,所有结点相互独立、毫无关联,无法实现队列的连续存储和有序操作。

三、整体框架

基于上述结点结构体,采用C++面向对象思想封装链式队列,将队列的指针属性作为私有成员隐藏封装,将队列所有基础操作封装为公有成员函数,实现数据封装隔离、操作模块化,符合C++类封装特性,规避C语言结构体操作数据裸露、安全性差的问题。

1. 完整框架代码

cpp 复制代码
// 链式队列类(C++ 封装版)
class LinkQueue
{
private:
    // 队首指针
    LQNode* front;
    // 队尾指针
    LQNode* rear;

public:
    // 1. 构造函数(初始化)
    LinkQueue();
    // 2. 入队
    bool Push(ELEMETYPE val);
    // 3. 出队
    bool Pop();
    // 4. 获取队头元素值
    ELEMETYPE Front();
    // 5. 获取有效元素个数
    int size();
    // 6. 判空
    bool Empty();
    // 7. 销毁(析构函数)
    ~LinkQueue();
    // 8. 打印
    void Show();
};

2. 私有成员变量

front队头指针:永久定位队列第一个结点,所有出队、获取队头元素的操作都依赖该指针,无front指针无法定位队头位置,无法完成删除和取值操作,封装为私有防止外部随意修改导致队列结构错乱。

rear队尾指针:永久定位队列最后一个结点,所有入队添加元素操作依赖该指针,无需每次入队从头遍历找尾部,保证入队操作时间复杂度为O(1),提升运行效率。

3. 公有成员函数

全覆盖链式队列生命周期全流程操作,从初始化、元素增删、数据查询、状态判断到内存销毁、数据打印,八大核心函数各司其职,无冗余功能、无缺失操作。构造函数负责队列初始化、析构函数负责内存释放,Push/Pop负责核心元素增减,Front/size/Empty负责数据状态查询,Show负责可视化遍历,完整适配链式队列所有基础使用场景。

四、成员函数详解

基于类框架逐一实现所有成员函数,每个函数均先讲解核心实现逻辑、标注边界条件与特殊异常情况,再附上带详细注释的完整实现代码,重点防护空队列操作、单结点出队、内存泄漏等核心风险点,保证代码健壮稳定。

1. 构造函数

实现核心思路:链式队列创建初始对象时,默认无任何有效结点,为空队列状态,只需将队头front和队尾rear两个核心指针置空,杜绝野指针问题。

边界与特殊情况处理:构造函数自动调用,无需手动传参,无需处理异常输入,仅需保证初始队列严格为空,为后续入队操作做好铺垫。

cpp 复制代码
LinkQueue::LinkQueue()
{
    front = nullptr;
    rear = nullptr;
}

2. 判空函数

实现核心思路:链式队列为空的判定标准是队头front指针为空,即可判定队列无任何有效结点。

边界与特殊情况处理:所有出队、取值操作前置调用,作为安全防护第一道关卡,防止空队列执行非法操作导致程序崩溃。

cpp 复制代码
bool LinkQueue::Empty()
{
    return front == nullptr;
}

3. 入队函数 Push

实现核心思路:动态申请新结点内存,给新结点赋值并置空后继指针;先判断队列是否为空,空队列则头尾指针同时指向新结点,非空队列则修改队尾结点后继指向新结点,再移动rear指针至新队尾。

边界与特殊情况处理:空队列第一次入队单独特殊处理,避免front指针悬空;内存申请失败直接退出,防止非法内存访问。

cpp 复制代码
bool LinkQueue::Push(ELEMETYPE val)
{
    // 1. 创建新节点
    LQNode* pnewnode = new LQNode;
    if (pnewnode == nullptr)
        exit(EXIT_FAILURE);
    // 2. 赋值
    pnewnode->data = val;
    pnewnode->next = nullptr;
    // 3. 空队列特殊处理
    if (Empty())
    {
        front = pnewnode;
        rear = pnewnode;
    }
    else
    {
        rear->next = pnewnode;
        rear = pnewnode;
    }
    return true;
}

4. 出队函数 Pop

实现核心思路:先调用判空函数防护,空队列直接返回;临时保存待删除队头结点,front指针后移完成逻辑出队;释放原队头结点内存,防止内存泄漏。

边界与特殊情况处理:空队列禁止出队,直接防护;队列只剩最后一个结点时,出队后必须将rear指针置空,避免野指针崩溃。

cpp 复制代码
bool LinkQueue::Pop()
{
    if (Empty())
        return false;

    LQNode* p = front;
    front = front->next;
    // 如果删除后队列为空,rear 置空
    if (front == nullptr)
    {
        rear = nullptr;
    }

    delete p;
    p = nullptr;
    return true;
}

5. 获取队头元素 Front

实现核心思路:前置判空断言防护,直接返回队头指针指向结点的数据域,只读不删除元素。

边界与特殊情况处理:禁止空队列调用取值操作,断言强制校验,防止访问空指针内存导致程序崩溃。

cpp 复制代码
ELEMETYPE LinkQueue::Front()
{
    assert(!Empty());
    return front->data;
}

6. 统计元素个数

实现核心思路:定义遍历指针从队头开始循环遍历所有结点,逐个计数,遍历结束返回总元素个数。

边界与特殊情况处理:空队列直接计数为0,遍历指针从头至尾完整遍历,不遗漏任何结点。

cpp 复制代码
int LinkQueue::size()
{
    int count = 0;
    for (LQNode* p = front; p != nullptr; p = p->next)
    {
        count++;
    }
    return count;
}

7. 打印遍历函数

实现核心思路:遍历指针从队头至队尾,依次输出每个结点数据,可视化展示队列元素存储顺序。

边界与特殊情况处理:空队列无元素可打印,直接无输出,不执行无效遍历操作。

cpp 复制代码
void LinkQueue::Show()
{
    for (LQNode* p = front; p != nullptr; p = p->next)
    {
        cout << p->data << " ";
    }
    cout << endl;
}

8. 析构销毁函数

实现核心思路:循环遍历所有结点,逐个释放结点内存,直至队列为空,最后将头尾指针置空。

边界与特殊情况处理:杜绝内存泄漏,对象销毁时自动释放所有动态申请内存,防止野指针残留。

cpp 复制代码
LinkQueue::~LinkQueue()
{
    LQNode* p = front, * q = nullptr;
    while (p != nullptr)
    {
        q = p->next;
        delete p;
        p = q;
    }
    front = rear = nullptr;
}

五、功能测试

为验证链式队列代码健壮性、边界防护有效性,无需粘贴完整重复代码,仅划分正常业务测试场景异常容错测试场景,针对性测试核心功能与边界防护逻辑,确保队列常规操作可用、非法操作不崩溃。

1. 正常业务测试

  • 批量入队测试:连续添加多个整型元素,调用打印函数查看队列元素顺序,验证入队仅在队尾添加、元素排序符合先进先出初始规则,头尾指针移动正常无错乱。
  • 单次/连续出队测试:多次执行出队操作,每次出队后打印队列、查看队头元素和元素总个数,验证出队仅删除队头元素,元素数量同步递减,队列结构无异常。
  • 队头元素获取测试:入队、出队前后分别读取队头元素,验证取值操作只读不删数据,队头元素随出队操作正常更新。
  • 元素个数统计测试:不同操作后统计队列有效元素数量,验证计数遍历逻辑准确,个数与实际入队出队次数匹配。

2. 异常边界测试

  • 空队列直接出队测试:队列初始化后未入队任何元素,直接调用出队函数,验证空队列防护逻辑生效,程序不崩溃、不报错,直接拦截非法出队操作。

  • 空队列获取队头元素测试:队列为空时调用取值函数,验证断言防护生效,杜绝空指针访问,程序友好拦截异常操作。

  • 单结点队列出队测试:队列仅保留最后一个有效结点,执行出队操作,验证出队后头尾指针同步置空,无野指针问题,队列正常回归空状态。

  • 队列销毁后操作测试:调用析构函数销毁队列后,执行打印、计数操作,验证内存释放彻底,无内存泄漏、无程序闪退问题。

六、全文总结

本次完成C语言链式队列源码转C++面向对象类封装完整实现,先搭建链式队列底层结点结构体,明确数据存储与结点关联的核心设计原理;再基于结构体封装C++队列类,私有化核心指针成员、模块化规划八大核心成员函数,搭建规范类框架;随后逐一对每个成员函数拆解实现思路、标注边界特殊情况,搭配带详细注释的实现代码,重点防护空队列操作、单结点出队、内存泄漏等关键风险;最后分正常业务场景和异常边界场景针对性测试,全方位验证链式队列功能稳定性与容错性。链式队列依托动态结点和头尾指针实现,无固定容量限制、无假溢出问题,操作效率稳定在O(1),代码结构清晰、封装性强,是数据结构中队列模块的核心必学内容,后续将继续更新顺序循环队列相关实现与对比内容。

相关推荐
晔子yy1 小时前
[JAVA探索之路]带你从零开始实现线程池
java·开发语言
爱上好庆祝1 小时前
学习js的第六天(js基础的结束)
开发语言·前端·javascript·学习·ecmascript
Rust研习社2 小时前
Rust 的 Box、Rc、Arc 到底怎么选?
开发语言·后端·rust
yqcoder2 小时前
JS 类型检测双雄:typeof vs instanceof 深度解析
开发语言·javascript·ecmascript
NEGl DRYN2 小时前
Go基础之环境搭建
开发语言·后端·golang
AI木马人2 小时前
20.人工智能实战:大模型项目如何从 Demo 走向生产?一套可落地的上线验收清单与工程治理方案
java·开发语言·人工智能
CandyU22 小时前
Unity —— 反射
java·开发语言
初心未改HD2 小时前
Go Modules:依赖管理的完全指南
开发语言·golang
楼田莉子2 小时前
仿照Muduo的高并发服务器:EventLoop模块及与TimeWheel模块联调
java·开发语言