数据结构入门:手把手实现队列

一、队列的基本概念

1.1 什么是队列

队列是一种特殊的线性表,它的限制是:只允许在一端进行插入操作,在另一端进行删除操作

  • 执行插入操作的一端,叫做队尾
  • 执行删除操作的一端,叫做队头

生活中最贴切的例子就是排队买奶茶:先来的人站在队伍最前面,先拿到奶茶离开;新来的人只能排在队伍最后面,不能插队。整个过程严格遵守「先来先服务」的规则。

1.2 核心规则:先进先出

队列中的数据元素严格遵守 FIFO(First In First Out,先进先出) 原则:

  • 最先进入队列的元素,会最先被取出
  • 最后进入队列的元素,会最后才被取出

两个核心操作有专门的名称:

  • 入队:往队尾插入元素的操作
  • 出队:从队头删除元素的操作

二、队列的底层结构选型

和栈一样,队列也可以用数组或链表实现,但两者的效率差异很大,链表结构实现队列更优,原因非常直观:

  • 如果用数组实现:入队是尾插,效率很高;但出队是头删,需要把后面所有元素依次向前挪动,时间复杂度是 O (n),数据量大时效率很低。即便用下标移动避免数据挪动,也会出现「前面空间空了却用不了」的假溢出问题。
  • 如果用链表实现:出队对应头删、入队对应尾插,只要我们同时维护队头、队尾两个指针,两种操作都能在 O (1) 时间内完成,而且内存按需分配,没有空间浪费。

因此我们用带头尾指针的单链表来实现队列。

三、队列的完整 C 语言实现

我们按照工程规范拆分代码:头文件存放结构体定义和函数声明,源文件存放具体实现。

3.1 头文件 Queue.h:定义结构与接口

我们先定义链表结点的结构,再把队头指针、队尾指针、元素个数封装成一个队列结构体。这样做的好处是:不用每次传两个指针,操作头尾都很方便,也符合封装的思想。

// Queue.h
#pragma once  // 防止头文件重复包含

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

// 队列中存储的数据类型,修改类型只需改动这一行
typedef int QDataType;

// 链表结点结构:存储数据 + 指向下一个结点的指针
typedef struct QueueNode
{
    QDataType val;
    struct QueueNode* next;
} QNode;

// 队列结构:封装头尾指针与元素个数
typedef struct Queue
{
    QNode* phead; // 队头指针,指向第一个结点
    QNode* ptail; // 队尾指针,指向最后一个结点
    int size;     // 记录队列中有效元素的个数
} Queue;

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

// 销毁队列,释放所有结点内存
void QueueDestroy(Queue* pq);

// 入队:队尾插入元素 x
void QueuePush(Queue* pq, QDataType x);

// 出队:删除队头元素
void QueuePop(Queue* pq);

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

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

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

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

3.2 源文件 Queue.c:接口逐一实现

下面我们逐个实现每个函数,重点标注新手容易踩坑的边界情况。

① 初始化队列

空队列状态下,头尾指针都为 NULL,元素个数为 0。

void QueueInit(Queue* pq)
{
    assert(pq); // 断言:防止传入空指针
    
    pq->phead = NULL;
    pq->ptail = NULL;
    pq->size = 0;
}
② 销毁队列

队列的每个结点都是动态申请的,销毁时必须逐个遍历释放,不能只释放队列结构体,否则会造成严重的内存泄漏。

void QueueDestroy(Queue* pq)
{
    assert(pq);
    
    QNode* cur = pq->phead;
    while (cur)
    {
        QNode* next = cur->next; // 先保存下一个结点的地址
        free(cur);               // 释放当前结点
        cur = next;              // 继续处理下一个
    }
    
    // 释放完所有结点后,指针置空,计数归零
    pq->phead = NULL;
    pq->ptail = NULL;
    pq->size = 0;
}
③ 入队(队尾插入)

入队需要分两种情况处理:队列为空时、队列已有元素时。

  • 空队列:新结点既是队头也是队尾
  • 非空队列:让当前尾结点的 next 指向新结点,再把尾指针更新为新结点
void QueuePush(Queue* pq, QDataType x)
{
    assert(pq);
    
    // 1. 创建新结点并赋值
    QNode* newNode = (QNode*)malloc(sizeof(QNode));
    if (newNode == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    newNode->val = x;
    newNode->next = NULL;
    
    // 2. 插入到队尾
    if (pq->ptail == NULL) // 队列为空
    {
        pq->phead = newNode;
        pq->ptail = newNode;
    }
    else // 队列非空
    {
        pq->ptail->next = newNode;
        pq->ptail = newNode;
    }
    
    pq->size++;
}
④ 出队(队头删除)

要处理「只剩一个结点」的特殊情况,否则尾指针会变成野指针。

  • 只剩一个结点:释放后队头、队尾指针都要置空
  • 多个结点:保存第二个结点,释放头结点,再更新队头指针
void QueuePop(Queue* pq)
{
    assert(pq);
    assert(!QueueEmpty(pq)); // 空队列禁止出队
    
    if (pq->phead->next == NULL) // 只剩一个结点
    {
        free(pq->phead);
        pq->phead = NULL;
        pq->ptail = NULL; // 必须置空,否则ptail会变成野指针
    }
    else // 多个结点
    {
        QNode* next = pq->phead->next;
        free(pq->phead);
        pq->phead = next;
    }
    
    pq->size--;
}
⑤ 获取队头元素

直接返回队头结点的数据,空队列禁止取值。

QDataType QueueFront(Queue* pq)
{
    assert(pq);
    assert(!QueueEmpty(pq));
    
    return pq->phead->val;
}
⑥ 获取队尾元素

直接返回队尾结点的数据。

QDataType QueueBack(Queue* pq)
{
    assert(pq);
    assert(!QueueEmpty(pq));
    
    return pq->ptail->val;
}
⑦ 判断队列是否为空

队头指针为 NULL 时,队列为空;也可以用 size == 0 判断,两者等价。

bool QueueEmpty(Queue* pq)
{
    assert(pq);
    return pq->phead == NULL;
}
⑧ 获取元素个数

直接返回我们维护的 size 即可,不用遍历计数。

int QueueSize(Queue* pq)
{
    assert(pq);
    return pq->size;
}

3.3 功能测试示例

写一个主函数,验证我们实现的队列是否符合先进先出的规则:

int main()
{
    Queue q;
    QueueInit(&q);
    
    // 依次入队 1、2、3、4
    QueuePush(&q, 1);
    QueuePush(&q, 2);
    QueuePush(&q, 3);
    QueuePush(&q, 4);
    
    printf("队列大小:%d\n", QueueSize(&q));    // 输出 4
    printf("队头元素:%d\n", QueueFront(&q));   // 输出 1(最先入队的在队头)
    printf("队尾元素:%d\n", QueueBack(&q));    // 输出 4(最后入队的在队尾)
    
    // 执行一次出队
    QueuePop(&q);
    printf("出队后队头:%d\n", QueueFront(&q)); // 输出 2
    
    // 用完销毁,释放所有结点内存
    QueueDestroy(&q);
    return 0;
}

运行结果完全符合先进先出的特性:最先入队的 1 位于队头,出队后队头变为第二个入队的 2。

四、总结

  1. 队列的核心特征是先进先出(FIFO),队尾入队、队头出队,所有操作都只在两端进行。
  2. 通用实现首选带头尾指针的单链表,入队、出队时间复杂度都是 O (1)。
  3. 实现时的关键注意点:
    • 入队要处理空队列的特殊情况,同时更新头尾指针
    • 出队要处理只剩一个结点的情况,避免尾指针变成野指针
    • 销毁时必须逐个遍历释放结点,防止内存泄漏
  4. 固定容量场景下可以使用数组实现循环队列,通过牺牲一个位置区分空满,空间复用效率更高。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值