数据结构——队列的概念解析与核心代码

队列(Queue)

队列像食堂排队打饭:先来的人先打到饭走人,后来的人只能站队尾,先进先出(FIFO),绝对公平。

核心思想

队列是受限的线性表:一端(队尾)只能进(入队 Push),另一端(队头)只能出(出队 Pop),中间不能动。和栈相反,栈是 LIFO,队列是 FIFO。

我用带头尾指针的链式队列实现(也可以用数组):

  • phead:指向队头节点(出队端)。
  • ptail:指向队尾节点(入队端)。
  • size:节点数量。

关键设计:用 size 维护长度。如果没有 size,求队列长度要遍历 O(n);有了 size,入队出队时顺手 ++/--,判空和求长度都 O(1)。另外头尾双指针让入队(尾插)和出队(头删)都 O(1)——如果只有头指针,尾插要遍历找尾就 O(n) 了。

数据结构定义

typedef struct QueueNode       // 链队节点
{
    QDatatype data;
    struct QueueNode *next;
}QNode;

typedef struct Queue           // 队列管理结构:头尾指针 + size
{
    QNode *phead;              // 队头
    QNode *ptail;              // 队尾
    int size;                  // 节点数
}Que;

内存模型(入队了 1,2,3,出队了 1 次,剩 2,3):

phead                                  ptail
  ↓                                      ↓
[2|next] -> [3|next] -> NULL
 队头                      队尾
 出队从 phead 删          入队在 ptail 后插
size = 2

两段式设计:QNode 是链表节点,Que 是管理结构(包住头尾指针和 size)。外部只持有 Que,不直接碰 QNode,封装性好。

关键操作实现

初始化 QueInit

void QueInit(Que *pq)
{
    assert(pq);
    pq->size = 0;
    pq->phead = pq->ptail = NULL;    // 头尾同时置空
}

逐行解释:空队列没有节点,头尾指针都置 NULL。必须同时置空:如果只置 phead 不置 ptail,ptail 残留野指针,第一次入队判 ptail==NULL 会误判。

入队 QuePush

void QuePush(Que *pq, QDatatype x)
{
    assert(pq);
    QNode *newnode = (QNode *)malloc(sizeof(QNode));
    if (newnode == NULL)        // malloc 检查
    {
        perror("malloc fail");
        return;
    }
    newnode->next = NULL;
    newnode->data = x;
    if (pq->ptail == NULL)      // 空队列
    {
        pq->phead = pq->ptail = newnode;   // 头尾都指向新节点
    }
    else                        // 非空,尾插
    {
        pq->ptail->next = newnode;   // 原尾的 next 指向新节点
        pq->ptail = newnode;          // 更新尾指针
    }
    pq->size++;
}

逐行解释:入队 = 尾插。空队列时新节点既是队头也是队尾,必须同时更新 phead 和 ptail(只更新一个会丢指针)。非空时让原尾节点的 next 指向新节点,再更新 ptail 指向新尾。size++ 维护长度。

陷阱:空队列入队只更新 ptail 忘更新 phead,导致 phead 仍为 NULL,出队时 phead->data 段错误。

出队 QuePop

void QuePop(Que *pq)
{
    assert(pq);
    assert(pq->size != 0);      // 空队列不能出

    if (pq->phead->next == NULL)   // 只剩一个节点
    {
        free(pq->phead);
        pq->phead = pq->ptail = NULL;   // 头尾都置空,变空队列
    }
    else
    {
        QNode *next = pq->phead->next;  // 先存第二个节点
        free(pq->phead);                // 再删队头
        pq->phead = next;               // 头指针后移
    }
    pq->size--;
}

逐行解释:出队 = 头删。分两种情况:

  • 只剩一个节点:删完后队列变空,必须把 ptail 也置 NULL,否则 ptail 变悬空指针,下次入队 ptail->next 段错误。
  • 多个节点:用 next 先存住原队头的下一个(防 free 后丢失),再 free 队头,头指针后移。

陷阱(最高频):只剩一个节点出队时,忘置空 ptail → 悬空指针。这是链式队列最经典的 bug。

取队头 QueFront / 取队尾 QueBack

QDatatype QueFront(Que *pq)
{
    assert(pq);
    assert(pq->phead);          // 队列不能空
    return pq->phead->data;
}

QDatatype QueBack(Que *pq)
{
    assert(pq);
    assert(pq->ptail);          // 队列不能空
    return pq->ptail->data;
}

逐行解释:双指针的红利——队头队尾都 O(1) 可取。assert 防止空队列取数据。注意 QueBack 是链式队列的优势,数组循环队列取队尾稍麻烦。

判空 QueEmpty

bool QueEmpty(Que *pq)
{
    assert(pq);
    return pq->size == 0;       // 用 size 判空,O(1)
}

逐行解释:用 size 判空,也可以用 phead == NULL,等价。size 维护后判空和求长都 O(1)。

求长度 Quesize

int Quesize(Que *pq)
{
    assert(pq);
    return pq->size;           // 直接返回,O(1)
}

逐行解释:size 的价值——不用遍历。命名问题:这里叫 Quesize(小写 s),但项目其他函数是 QuePush/QuePop(大写 P),命名风格不统一,应改成 QueSize。

销毁 QueDestroy(已修复)

void QueDestroy(Que *pq)
{
    assert(pq);
    QNode *cur = pq->phead;
    while (cur)
    {
        QNode *next = cur->next;
        free(cur);          // 先释放当前节点,再移动到下一个,否则会内存泄漏
        cur = next;
    }
    pq->phead = pq->ptail = NULL;
    pq->size = 0;
}

逐行解释:逐个 free 节点。必须先存 next 再 free(cur),否则 free 后 cur->next 读不到(UAF)。

修复前的 bug:原来写成 cur = NULL; cur = next;——先 cur = NULL 再 cur = next,等于什么都没释放(free 调用根本没发生),所有节点内存泄漏。正确是 free(cur) 再 cur = next。这是「想置空却忘了 free」的典型错误。

复杂度分析

操作时间复杂度说明
入队 PushO(1)尾插,ptail 直接定位
出队 PopO(1)头删,phead 直接定位
取队头 / 队尾O(1)双指针
判空 / 求长度O(1)维护 size
销毁O(n)逐个释放

对比数组队列:数组队列出队要搬移所有元素 O(n)(除非用循环队列);链式队列头删 O(1),所以队列更适合用链表实现(栈更适合用数组,因为栈只在顶操作,数组天然合适)。

常见陷阱清单

  1. 只剩一个节点出队不置空 ptail → 悬空指针,下次入队段错误(最高频)。
  2. 空队列判 ptail==NULL 入队时只更新一个指针 → 丢队头或队尾。
  3. 销毁只置空不 free(修复前的 bug)→ 内存泄漏。
  4. 销毁先 free 后取 next → UAF。
  5. #include <queue.h> 用尖括号:自定义头文件应用双引号 "queue.h",尖括号是给系统头文件的。
  6. 命名不统一:Quesize 应改 QueSize。

我的实现 vs 教科书实现

  • 设计正确:头尾双指针 + size,入队出队取头尾全 O(1),结构标准。
  • 已修复销毁泄漏:free(cur) 顺序正确。
  • 残留问题:test.c 用 #include <queue.h> 尖括号(应用引号);Quesize 命名不统一。
  • 缺接口:没有从队头遍历打印的接口(测试代码直接在 main 里循环出队打印)。
  • vs 循环队列:链式队列无限扩容但每节点有指针开销;循环队列用定长数组 + 取模,省空间但要处理判空判满。

拓展知识点

变体与进阶

  • 循环队列(Circular Queue):用定长数组实现,front/rear 指针取模复用空间。难点是判空判满——空是 front==rear,满也是 front==rear,冲突!两种解法:①牺牲一个单元(rear+1==front 为满);②额外用 size 计数(本项目链队的思路)。
  • 双端队列 Deque:两端都能入队出队,std::deque 底层是分块数组,支持 O(1) 头尾操作 + 随机访问。
  • 优先队列(Priority Queue):出队按优先级而非时间,底层用堆(见 Binary_Tree 模块),O(log n)。
  • 阻塞队列:线程间通信的基础,队空时取阻塞、队满时入阻塞。

常见考点(必刷)

  • 用两个栈实现队列:入队压栈 A,出队时若栈 B 空,把 A 全倒进 B 再弹出。
  • 循环队列判空判满:牺牲单元法 vs size 计数法。
  • BFS 层序遍历:队列是 BFS 的核心数据结构(见 Binary_Tree 的 TreeLevelOrder)。
  • 滑动窗口最大值:单调队列,O(n)。

与其他结构的关系

队列是 BFS、层序遍历、任务调度的基础。本项目 Binary_Tree 模块复用了一个 queue(存 Treenode*)来做层序遍历和完全二叉树判断——这是「数据结构组合使用」的典型。队列和栈是受限线性表的两兄弟,可互相模拟。

一句话记忆法

队列:先进先出(FIFO),尾入头出;头尾双指针 + size 让操作全 O(1),单节点出队别忘置空 ptail,销毁先存 next 再 free。


#DataStructure #CS #Queue #FIFO #受限线性表

上一篇: 解析预处理与编译的过程
下一篇: 数据结构——栈的概念理解与代码实现
DDXYcoder
博客等级 码龄1年 140粉丝 29原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值