【数据结构】操作受限的线性表,队列的具体实现

简介: 【数据结构】操作受限的线性表,队列的具体实现

前言

  队列和栈一样,同样是操作受限的线性表,在日常生活中的体现也很多,所以学习队列也是必不可少的。本篇文章将会详细介绍队列的具体实现,去解释每行代码的意思,希望对你有所帮助。话不多说,直接上菜。

  文章末尾附带源码。


一、初识队列

  队列,顾名思义,和平时就会遇到的排队一样。那么排队有什么特点吗,我们都知道,排队就是为了 “先到先得” 先来排队的会先完成自己的事并第一个离开队伍。所以队列也一样,他的核心思想就是先进先出先进的数据肯定是比后进的数据要先出的。栈跟队列是相反的,栈是 后进先出 的线性表,队列是 先进先出 的线性表,千万不要记混了。

  一般规定在队尾插入,在队头删除,也就是说在队头出数据,在队尾入数据。队列同样可以使用顺序表或者链表实现,如果使用顺序表,也就是数组来实现,不管是在哪端插入删除,都无法避免将整个数组给挨个移动,效率比较低。如果使用链表的话,在哪端插入删除都有较好的解决方法,效率较高。所以本篇文章将会使用最常见的单链表的方式来实现队列。

二、头文件的编写

  好的代码可读性是非常高的,分工明确,所以我们将库函数源文件、定义的结构体和函数声明放在一个头文件里面。我们创建一个头文件,叫做 “Queue.h” 。

1.引入库函数头文件

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

2.定义队列结构体

// 重定义队列的数据类型
typedef int QDataType;
// 队列元素的结构体
typedef struct QueueNode
{
  // 队列元素的数据域
  QDataType val;
  // 队列元素的指针域
  struct QueueNode* next;
}QNode;

  我们使用 typedef 重定义队列的数据类型,后文的数据类型都被替换为 QDataType ,以方便以后改变数据类型时只需要改变此一行代码即可。由于我们是使用链表的方式来实现队列的,我们都知道,链表是通过指针来连接数据与数据的,所以在创建队列元素的结构体时,不仅仅需要存储元素的值,还需要一个指针来找到下一个元素。

3.定义传参结构体

// 传参结构体
typedef struct Queue
{
  // 有一个指向队列的首元结点(队头)的指针
  QNode* phead;
  // 有一个指向队列的尾结点(队尾)的指针
  QNode* ptail;
  // 队列的元素个数
  int size;
}Queue;

  队列的基本特性就是在队尾插入,在队头删除,不会改变中间的值。所以队尾和队头是需要经常使用的,不妨我们直接记录一下队列的队尾和队头,那么找尾再也不用遍历链表了。于是我们创建一个结构体,这个结构体里面储存着队头和队尾的位置,还顺便储存着队列的大小。两个指针是指向队列元素的,可以通过指针创建队列元素,所有有了指向队头的头指针,就可以创建队列。这个结构体在后面会有大用。

4.声明功能函数

// 初始化队列
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);
// 判断队列是否为空
bool QueueEmpty(Queue* pq);
// 查询队列元素个数
int QueueSize(Queue* pq);

  在一般情况下,对链表操作时,可能改变头指针的函数传参必须传入头指针的地址,通过二级指针来实现功能。但我们早在前面以及使用结构体包含了头指针以及尾指针,如果要改变头指针或者尾指针或者队列的元素个数,相当于在改变结构体,所以传入结构体的指针就能实现。

三、主函数文件的编写

  同样的操作,我们将测试函数以及主函数放在同一个文件里面。我们创建一个源文件,叫做 “test.c” 。

1.包含头文件

#include"Queue.h"

2.编写测试函数

void test()
{
    // 创建一个传参结构体成员
  Queue q;
  // 初始化这个成员
  QueueInit(&q);
  // 在队尾入队列
  QueuePush(&q, 1);
  // 在队尾入队列
  QueuePush(&q, 2);
  // 在队尾入队列
  QueuePush(&q, 3);
  // 在队尾入队列
  QueuePush(&q, 4);
  // 在队尾入队列
  QueuePush(&q, 5);
  // 如果队列不为空
  while (!QueueEmpty(&q))
  {
      // 打印队头元素
    printf("%d ", QueueFront(&q));
    // 在队头出队列
    QueuePop(&q);
  }
  printf("\n");
  // 销毁
  QueueDestroy(&q);
  return 0;
}

  测试用例仅作参考,可自拟测试用例。

四、功能函数的编写

  我们创建一个源文件,叫做 “Queue.c” 。

1.包含头文件

#include"Queue.h"

2.初始化

// 传入传参结构体成员的地址
void QueueInit(Queue* pq)
{
  // pq是指向传参结构体的指针,只要结构体创建了,那pq就不可能为空,为空说明传参错误或者结构体没创建
  assert(pq);
  
  // 让指向首元结点的头指针和指向尾结点的尾指针置空,说明队列为空
  pq->phead = pq->ptail = NULL;
  // 队列目前有效数据为0
  pq->size = 0;
}

3.销毁

void QueueDestroy(Queue* pq)
{
    // pq不可能为空,可以断言一下以免出现小差错
  assert(pq);
    // 如果pq指向的结构体里面的phead不为空,即存在结点
  while (pq->phead)
  {
      // 创建一个临时指针变量指向首元结点
    QNode* tmp = pq->phead;
    // 让phead指向第二个结点
    pq->phead = pq->phead->next;
    // 释放首元结点空间
    free(tmp);
    // 释放空间后指针置空是好习惯
    tmp = NULL;
  }
  // 最后没有结点的时候,把尾结点置空
  pq->ptail = NULL;
  // 让有效数据大小为0
  pq->size = 0;
}

  销毁队列的方式其实就是链表的头删,使用临时指针变量指向待销毁的空间,然后使原本指向该结点的指针指向下一个准备销毁的结点,最后销毁即可。在这里面,每销毁一个结点,头指针就会根据结点的指针域找到下一个结点的位置,销毁到最后一个结点时,头指针已经通过指针域被赋值为 NULL 了,由于销毁完了后,尾指针还指向原本尾结点的空间,数据个数也还没有改变,所以销毁的最后是需要把参数给修改回初始值的。

4.入队列

void QueuePush(Queue* pq, QDataType x)
{
  assert(pq);
  // 为新结点开辟空间
  QNode* newNode = (QNode*)malloc(sizeof(QNode));
  // 开辟失败时
  if (newNode == NULL)
  {
      // 弹出反馈
    perror("malloc fail");
    // 终止程序
    exit(-1);
  }
  // 为新结点赋值
  newNode->val = x;
  newNode->next = NULL;
  // 当队列为空时
  if (pq->ptail == NULL)
  {
      // 头指针指向新结点
    pq->phead = pq->ptail = newNode;
  }
  // 当队列非空时
  else
  {
      // 原尾结点的指针域指向新结点
    pq->ptail->next = newNode;
    // 尾指针指向新结点
    pq->ptail = newNode;
  }
  // 队列的有效个数+1
  ++pq->size;
}

  入队列操作只能是在队尾操作,我们可以通过尾指针去找到尾结点,然后在尾结点后面插入即可,需要注意的是,如果队列中没有数据的话,新结点就相当于即是尾结点又是首元结点,这个情况需要改变两个指针。

5.出队列

void QueuePop(Queue* pq)
{
  assert(pq);
  // 队列为空不能删除
  assert(pq->phead);
    // 临时指针指向首元结点
  QNode* tmp = pq->phead;
  // 当队列中只有一个结点时
  if (pq->phead == pq->ptail)
  {
      // 将两个指针置空
    pq->phead = pq->ptail = NULL;
  }
  // 当队列中不止一个结点时
  else
  {
      // 让头结点指向第二个结点
    pq->phead = pq->phead->next;
  }
  // 有效数据个数-1
  --pq->size;
  // 释放原首元结点的空间
  free(tmp);
  // 释放空间后指针置空
  tmp = NULL;
}

  出队列与入队列一样,也要判断边界情况,当队列中没有元素时,无法进行出队列操作,当队列中只有一个元素时,出的元素就是首元结点,出完后队列变成空队列。

6.查询队头数据

QDataType QueueFront(Queue* pq)
{
  assert(pq);
  // 队列不为空才有队头数据
  assert(pq->phead);
    
    // 根据头指针找到队头数据
  return pq->phead->val;
}

  由于我们的结构体里面包含了头指针,而头指针指向首元结点,首元结点就是队头数据,所以使用头指针便可找到队头数据。

7.查询队尾数据

QDataType QueueBack(Queue* pq)
{
  assert(pq);
  // 也是指队列不为空才能有队尾数据
  assert(pq->ptail);
    
    // 根据尾指针找到队尾数据
  return pq->ptail->val;
}

  结构体里面不仅仅包含了头指针,还包含了尾指针,尾指针指向尾结点,尾结点就是队尾数据,所以使用尾指针便可找到队尾数据。

8.判断队列是否为空

bool QueueEmpty(Queue* pq)
{
  assert(pq);
    // 返回队列有效数据的个数是否等于0来判断队列是否为空
  return pq->size == 0;
}

   size 的值就是队列中的有效数据个数,如果队列中有效数据的个数为0,那就说明队列中无有效数据,就是空队列,反之就不是空队列。

9.查询队列中有效数据的个数

int QueueSize(Queue* pq)
{
  assert(pq);
    // 返回有效数据的个数
  return pq->size;
}

  我们在上面说过,size的值就是有效数据的个数,所以在这里直接返回size的值即可。

五、代码整合及结果演示

1.代码整合

  若在整合后出现某些函数不安全的错误,请在头文件里面加上下面这行代码。

#define _CRT_SECURE_NO_WARNINGS 1

1.头文件 Queue.h 部分

#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);
// 在队尾插入
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);

2.源文件 Queue.c 部分

#include"Queue.h"
void QueueInit(Queue* pq)
{
  // pq是指向传参结构体的指针,不可能为空,为空说明传参错误
  assert(pq);
  pq->phead = pq->ptail = NULL;
  pq->size = 0;
}
void QueueDestroy(Queue* pq)
{
  assert(pq);
  while (pq->phead)
  {
    QNode* tmp = pq->phead;
    pq->phead = pq->phead->next;
    free(tmp);
    tmp = NULL;
  }
  pq->ptail = NULL;
  pq->size = 0;
}
void QueuePush(Queue* pq, QDataType x)
{
  assert(pq);
  // 为新结点开辟空间
  QNode* newNode = (QNode*)malloc(sizeof(QNode));
  if (newNode == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  // 为新结点赋值
  newNode->val = x;
  newNode->next = NULL;
  // 当队列为空时
  if (pq->ptail == NULL)
  {
    pq->phead = pq->ptail = newNode;
  }
  // 当队列非空时
  else
  {
    pq->ptail->next = newNode;
    pq->ptail = newNode;
  }
  ++pq->size;
}
void QueuePop(Queue* pq)
{
  assert(pq);
  // 队列为空不能删除
  assert(pq->phead);
  QNode* tmp = pq->phead;
  // 当队列中只有一个结点时
  if (pq->phead == pq->ptail)
  {
    pq->phead = pq->ptail = NULL;
  }
  // 当队列中不止一个结点时
  else
  {
    pq->phead = pq->phead->next;
  }
  --pq->size;
  free(tmp);
  tmp = NULL;
}
QDataType QueueFront(Queue* pq)
{
  assert(pq);
  assert(pq->phead);
  return pq->phead->val;
}
QDataType QueueBack(Queue* pq)
{
  assert(pq);
  assert(pq->ptail);
  return pq->ptail->val;
}
bool QueueEmpty(Queue* pq)
{
  assert(pq);
  return pq->size == 0;
}
int QueueSize(Queue* pq)
{
  assert(pq);
  return pq->size;
}

3.源文件 test.c 部分

#include"Queue.h"
void test()
{
  Queue q;
  QueueInit(&q);
  QueuePush(&q, 1);
  QueuePush(&q, 2);
  QueuePush(&q, 3);
  QueuePush(&q, 4);
  QueuePush(&q, 5);
  while (!QueueEmpty(&q))
  {
    printf("%d ", QueueFront(&q));
    QueuePop(&q);
  }
  printf("\n");
  QueueDestroy(&q);
  return 0;
}
int main()
{
  test();
  return 0;
}

2.结果演示


总结

  本篇文章到这里就结束了,栈和队列部分其实还是比较简单的,总体难度不是很难。我只是使用了链表实现了队列,感兴趣的可以尝试使用顺序表来实现一下,相信对自己肯定会有帮助。

  文章虽然比较简单,但是也并非绝对不可能出现错误,如果在文章中发现了错误,欢迎指正,谢谢。如果这篇文章对你有所帮助,别忘了三连博主,您的支持是我最大的鼓励,我会尽可能的去写出更加优质的文章。谢谢。

目录
相关文章
|
14天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
90 9
|
17天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
初步认识栈和队列
初步认识栈和队列
58 10
|
1月前
|
存储 Java
数据结构第二篇【关于java线性表(顺序表)的基本操作】
数据结构第二篇【关于java线性表(顺序表)的基本操作】
30 6
|
1月前
|
存储 算法 定位技术
数据结构与算法学习二、稀疏数组与队列,数组模拟队列,模拟环形队列
这篇文章主要介绍了稀疏数组和队列的概念、应用实例以及如何使用数组模拟队列和环形队列的实现方法。
20 0
数据结构与算法学习二、稀疏数组与队列,数组模拟队列,模拟环形队列
|
17天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之王道第2.3章节之线性表精题汇总二(5)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
IKU达人之数据结构与算法系列学习×单双链表精题详解、数据结构、C++、排序算法、java 、动态规划 你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
【数据结构】-- 栈和队列
【数据结构】-- 栈和队列
16 0
|
1月前
探索数据结构:队列的的实现与应用
探索数据结构:队列的的实现与应用
|
1月前
|
存储 C语言
栈和队列题目练习
栈和队列题目练习
16 0
|
1月前
|
存储 算法 Java
【用Java学习数据结构系列】用堆实现优先级队列
【用Java学习数据结构系列】用堆实现优先级队列
29 0