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

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

前言

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

  文章末尾附带源码。


一、初识队列

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

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

二、头文件的编写

  好的代码可读性是非常高的,分工明确,所以我们将库函数源文件、定义的结构体和函数声明放在一个头文件里面。我们创建一个头文件,叫做 “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.结果演示


总结

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

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

目录
相关文章
|
8月前
|
前端开发 Java
java实现队列数据结构代码详解
本文详细解析了Java中队列数据结构的实现,包括队列的基本概念、应用场景及代码实现。队列是一种遵循“先进先出”原则的线性结构,支持在队尾插入和队头删除操作。文章介绍了顺序队列与链式队列,并重点分析了循环队列的实现方式以解决溢出问题。通过具体代码示例(如`enqueue`入队和`dequeue`出队),展示了队列的操作逻辑,帮助读者深入理解其工作机制。
265 1
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
11月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
535 77
|
10月前
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
245 11
|
10月前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
11月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
444 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
11月前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
241 9
|
11月前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
250 7
|
11月前
|
存储 算法 测试技术
【C++数据结构——线性表】求集合的并、交和差运算(头歌实践教学平台习题)【合集】
本任务要求编写程序求两个集合的并集、交集和差集。主要内容包括: 1. **单链表表示集合**:使用单链表存储集合元素,确保元素唯一且无序。 2. **求并集**:遍历两个集合,将所有不同元素加入新链表。 3. **求交集**:遍历集合A,检查元素是否在集合B中存在,若存在则加入结果链表。 4. **求差集**:遍历集合A,检查元素是否不在集合B中,若满足条件则加入结果链表。 通过C++代码实现上述操作,并提供测试用例验证结果。测试输入为两个集合的元素,输出为有序集合A、B,以及它们的并集、交集和差集。 示例测试输入: ``` a c e f a b d e h i ``` 预期输出:
318 7
|
11月前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
531 5

热门文章

最新文章