当前位置: 首页 > article >正文

C语言数据结构初阶(7)----队列

· CSDN的uu们,大家好。这里是C语言数据结构的第七讲。
· 目标:前路坎坷,披荆斩棘,扶摇直上。
· 博客主页: @姬如祎
  1. 队列的基础知识

队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。

2. 链式存储OR顺序存储

线性表有顺序存储和链式存储,栈是线性表,具有这两种存储方式。同样,队列作为一种特殊的线性表,也同样存在这两种存储方式。

2.1 队列顺序存储的不足

顺序存储的队列就是用数组哈!数组下标为0的一端即是队头。入队列就是在队尾插入一个数据,对于数组而言,不需要移动任何元素,因此时间复杂度是O(1)。

但是在出队列时如果徐翔浪费任何空间的话,就需要将全部数据前移。时间复杂度也就是O(N)。

当然你也可以参照数组模拟队列的思路,维护两个指针:front,rear。front指向队头的元素,rear指向队尾元素。出队列的时候仅需要将front加一即可。这样以来就有空间的浪费。如此看来使用顺序结构来实现队列不是很理想。如果已经确定了队列的元素个数,你可以使用循环队列,这样一来数组来实现队列就是非常完美的事了!关于循环队列,会在栈与队列刷题的那一节提及。

2.2 队列的链式存储

队列的链式存储结构,其实就是线性表的单链表,只不过他只能尾进头出而已,我们把它简称为链队列。

入队就是链表的尾插,我们知道如果不做任何处理的话,单链表尾插的时间复杂度是O(N),为了使得时间复杂度是O(1),我们需要维护一个指针,让他指向链表的尾节点,即是队尾。

出队就是链表的头删,这个操作的时间复杂度是O(1)。

队列的链式存储能够在O(1) 的时间复杂度内实现各种操作,也没有浪费空间。所以我们优先考虑用链表实现队列。

因为我们既要维护指向链表头结点的指针,又要维护指向链表尾节点的指针,并且我们还需要维护一个变量size来记录队列的大小,所以我们将这些变量封装在一个结构体里面,便于操作。

当然你也可以不这么做,那么在函数传参的时候你就需要传多个参数。并且涉及指针的改变,形参还必须是二级指针。

如果有不理解的地方请参考:
http://t.csdn.cn/QXmxD

是不是相当滴麻烦。那么我们就将它们封装成一个结构体吧!

3. 函数接口一览

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

//方便维护
typedef int QueueDataType;

//实现队列的链表的节点
typedef struct QueueNode
{
    struct QueueNode* next;
    QueueDataType data;
} QueueNode;

//将需要维护的head,tail,size封装成结构体
typedef struct Queue
{
    QueueNode* head;
    QueueNode* tail;
    int size;
} Queue;

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

//队列的销毁
void QueueDestory(Queue* pq);

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

//队尾增加元素
void QueuePush(Queue* pq, QueueDataType x);

//出队列
void QueuePop(Queue* pq);

//队列的元素个数
int QueueSize(Queue* pq);

//队头元素
QueueDataType QueueFront(Queue* pq);

//队尾元素
QueueDataType QueueBack(Queue* pq);

4. 函数接口的实现

4.1 void QueueInit(Queue* pq) 函数的实现

队列的初始化需要我们做什么呢?将我们封装的结构体里面的变量赋初值!注意对pq的断言,防止发生空指针的解引用。

//队列的初始化
void QueueInit(Queue* pq)
{
    //断言
    assert(pq);
    //赋初值
    pq->head = pq->tail = NULL;
    pq->size = 0;
}

4.2 void QueueDestory(Queue* pq) 函数的实现

销毁队列的主要目的:释放堆区开辟的空间,也就是释放单链表的每一个节点。这当然需要遍历呀!我们维护一个指针,例如:cur,用其遍历单链表,让后将遍历到的节点释放。注意保存释放节点的下一个节点就行。还别忘了将我们维护的三个变量:head,tail,size处理一下,防止出现野指针。

//队列的销毁
void QueueDestory(Queue* pq)
{
    //断言,防止空指针的解引用
    assert(pq);
    //cur指针遍历链表
    QueueNode* cur = pq->head;
    while (cur)
    {
        //保存cur指针的下一个节点
        QueueNode* next = cur->next;
        //释放cur指向的节点
        free(cur);
        //迭代
        cur = next;
    }
    //处理
    pq->head = pq->tail = NULL;
    pq->size = 0;
}

4.3 bool QueueEmpty(Queue* pq) 函数的实现

这个函数的实现有两种方式:

1:就是判断一下头指针是否为空指针就行,头指针为空就说明了链表没有节点,也就说明了队列为空。队列为空时,尾节点此时也是空指针的哦!

2:我们不是维护了一个变量size嘛,其记录了队列的大小,我们只需要判断size是不是0就ok啦!

//队列是否为空
bool QueueEmpty(Queue* pq)
{
    //断言,防止空指针的解引用
    assert(pq);
    //one way
    //return pq->size == 0;

    //another way
    return pq->head == NULL;
}

4.4 void QueuePush(Queue* pq, QueueDataType x) 函数的实现

前面也提到了,入队列的操作就是在链表的尾部插入节点。我们传入的是结构体的指针,想要改变结构体里面的head指针,tail指针是可以做到的,因此队列入数据时注意改变head指针和tail指针就行。

当队列为空时需要特殊处理一下:同时改变head和tail指向开辟的新节点。

//队尾增加元素
void QueuePush(Queue* pq, QueueDataType x)
{
    //断言,防止空指针的解引用
    assert(pq);
    //开辟新的节点
    QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
    if (newNode == NULL)
    {
        perror("QueuePush::malloc");
        exit(-1);
    }
    else
    {
        //数据域赋值
        newNode->data = x;
        newNode->next = NULL;
        //当队列为空时
        if (!pq->head)
        {
            pq->head = pq->tail = newNode;
        }
        else
        {
            //链表不为空时
            pq->tail->next = newNode;
            pq->tail = newNode;
        }
        pq->size++;
    }
}

4.5 void QueuePop(Queue* pq)的实现

出队列操作,就是链表的头删。理论上我们只需要释放头结点,改变head指针的指向就行。但是,当队列剩下最后一个元素时如果还是仅改变head指针的指向,那么tail指针就会是一个野指针,这样做显然是不正确的。因此我们需要对队列只有一个元素的情况进行特殊判断。

//出队列
void QueuePop(Queue* pq)
{
    //防止空指针的解引用
    assert(pq);
    //队列为空不允许出队列
    assert(!QueueEmpty(pq));

    //one way
    //对一个节点的情况处理
    if (pq->head == pq->tail)
    {
        free(pq->head);
        pq->head = pq->tail = NULL;
    }
    else
    {
        //队列中的元素大于一个
        QueueNode* next = pq->head->next;
        free(pq->head);
        pq->head = next;
    }
    //更新size
    pq->size--;

    //another way
    // 都是处理队列中只有一个元素的情况,只不过实现的方式不同
    //QueueNode* next = pq->head->next;
    //free(pq->head);
    //pq->head = next;
    //pq->size--;
    //if (pq->head == NULL)
    //    pq->tail = NULL;
}

4.6 int QueueSize(Queue* pq)函数的实现

我们只需要返回我们维护的size变量的值就行。

//队列的元素个数
int QueueSize(Queue* pq)
{
    assert(pq);
    return pq->size;
}

4.7 QueueDataType QueueFront(Queue* pq)函数的实现

我们维护了一个指针head,它指向的就是队头元素,返回该节点的打他即可。

//队头元素
QueueDataType QueueFront(Queue* pq)
{
    assert(pq);
    //队列为空不允许查看队头数据
    assert(!QueueEmpty(pq));
    return pq->head->data;
}

4.8 QueueDataType QueueBack(Queue* pq)函数的实现

我们维护了一个指针tail,它指向的就是队尾元素,我们只需要返回该节点的data即可。

//队尾元素
QueueDataType QueueBack(Queue* pq)
{
    assert(pq);
    assert(!QueueEmpty(pq));
    return pq->tail->data;
}


http://www.kler.cn/a/1913.html

相关文章:

  • Flask中@app.route()的methods参数详解
  • 怎么设置电脑密码?Windows和Mac设置密码的方法
  • 工厂防静电监控系统设备静电监控仪的关键作用
  • 再服务器上建立新的编译环境
  • 电脑丢失bcrypt.dll文件是什么原因?找不到bcrypt.dll文件修复办法来啦!
  • 功能很强大的单点登录协议 SAML 2.0详解
  • HBase高手之路4-Shell操作
  • 苹果发布无线充新专利,苹果Find My技术成为近几年苹果的重要创新
  • PCIE时钟解说
  • (数据结构)八大排序算法
  • 【绘图】比Matplotlib更强大:ProPlot
  • CLIP:一种基于视觉和语言相互关联的图像分类模型
  • 蓝桥杯刷题冲刺 | 倒计时20天
  • 数字图像处理 Delaunay三角剖分和Voronoi图
  • 从零实现深度学习框架——学习率调整策略介绍
  • 一文带你领略 WPA3-SAE 的 “安全感”
  • Java之链表(不带头结点,带头结点,迭代实现,递归实现)
  • 2023年 ZZU ACM 招新赛暨选拔赛题解
  • yolov8训练筷子点数数据集
  • 浏览器的组成部分
  • 2023美赛C题【分析思路+代码】
  • (只需五步)注册谷歌账号详细步骤,解决“此电话号码无法验证”问题
  • Android---动态权限申请
  • 算法学习之三步翻转法
  • 51单片机入门 -驱动 8x8 LED 点阵屏
  • 高速PCB设计指南(十五)