【数据结构】队列的实现

简介: 队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)的特点。

本篇总结队列是如何实现的,以及各方面的细节问题,比较链表与队列之间的不同,注意到不同的类型,要按照不同需求来定义结构,不能一成不变,印象刻板。


【队列】


⏰.队列的概念及结构


队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)的特点。


入队列:进行插入操作的一端叫做队尾


出队列:进行删除操作的一端叫队头


c63da82e7d8f4288ae1a11ff98267d43.png


⏰.队列的实现


队列可以用数组和链表来实现。但使用链表的结构更优一些,因为如果使用数组结构,删除数据需要挪动大量数据,效率比较低。


f2821d83b3fb47a58770336477ac4493.png


队列我们使用链表来实现


首先使用链表定义一个结点


我们都知道结点是由一个结构体定义的,这个给结构体里有个指向下一个结点地址的next和数据data。


但想一下,队列需要对链表进行尾删,也就是每次都要找到尾结点,将链表的头结点和尾结点传给出队列函数,然后进行出队列操作,这样传参数就传好几个,不如让我们将头结点的地址和尾结点的地址分装成一个新的结构体


想一想为什么这里要给个尾指针过去呢?


如果在实现单链表操作中也给尾指针呢?


单链表中给尾指针能完成操作吗?尾插操作需要尾指针,这个操作可以完成,但尾删呢?尾删操作你给个尾指针就能完成吗?当然不能,还需要尾指针前面的结点地址。传个尾指针过去只能完成部分操作,不能完成全部操作还不如不传,代码挫的一批。


但这里不一样,这里队列有它自己的规则:那就是只在一端进行入队,那就是队尾,也就是说在队尾只进行尾插,不进行尾删,所以这里传尾指针过去是可以完成队列所需要的操作的,


要区别单链表中只传一个头指针,而队列这里要传头指针和尾指针的不同,我们要按照需要来定义不同结构。


#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int QDatatype;
typedef struct QNode//定义结点结构
{
  struct QNode* next;
  QDatatype data;
}QNode;
typedef struct Queue//将头指针和尾指针分装起来,传给队列操作函数
{
  QNode* head;//链表头指针
  QNode* tail;//链表尾指针
  int size;//大小想一想传过去有什么好处?
}Queue;
//初始化队列
void  QueueInit(Queue* pq);
//销毁队列
void  QueueDestroy(Queue* pq);
//队尾入队列
void  QueuePush(Queue* pq,QDatatype x);
//队头出队列
void  QueuePop(Queue* pq);
//获取队列的有效数据的大小
void  QueueSize(Queue* pq);
//判断队列是否为空,如果为空返回非0,如果为非空返回0
bool  QueueEmpty(Queue* pq);
//获取队列头部元素
QDatatype  QueueFront(Queue* pq);
//获取队尾元素
QDatatype  QueueBack(Queue* pq);


🕐1.初始化队列


首先对传入的数据进行初始化,我们知道传过来的是个分装好的结构体,里面有链表头指针,尾指针,大小。


所以一开始我们需要将头指针尾指针和大小都置0;


//初始化队列
void  QueueInit(Queue* pq)
{
  assert(pq);//首先断言判断是否传错
  pq->head = pq->tail = NULL;
  pq->size = 0;
}


🕑2.队尾入队列


队尾入队列,实际上就是链表的尾插。


而这正是这操作我们将链表的尾指针也传过去,就是为了每次尾插不需要找尾,每次插入时更新下尾指针位置即可。


//入队列
void  QueuePush(Queue* pq, QDatatype x)
{
  assert(pq);//断言是否传错
  //入队,就是尾插一个结点,首先生成一个新结点
  QNode* newnode = (QNode*)malloc(sizeof(QNode));
  newnode->next = NULL;//需要置空,不然会出现野指针
  newnode->data = x;//将数据赋值
  if (newnode == NULL)//判断是否开辟成功
  {
    perror("malloc");
  }
  //因为为单链表,不带哨兵头,所以一开始head和tail都为空,第一步直接将结点赋值过去,然后后面才是真正的尾插
  if (pq->head == NULL)
  {
    pq->head = pq->tail = newnode;//将结点赋值过去
  }
  else
  {
    //尾插
    pq->tail->next = newnode;
    pq->tail = newnode;//找尾--更新
  }
  pq->size++;//插入一个数据size就要++
}


🕒3.队头出队列


对头出队列,本质上就是链表的头删,头删就是让头指针不断改变,然后让头指针前面的结点释放。


注意点1:


在删除数据之前我们都要对队列进行检查,看是否队列为空。


注意点2:要注意tail是指向最后一个结点的指针。


当head指向最后一个结点后释放,那么tail指向的空间被释放,那tail就变成野指针了,所以最后需要将tail置NULL。


有两中方式:


第一种:只管头删,最后再将tail置NULL

第二种:一开始就考虑二种情况,最后一个结点之前,和最后一个结点,即分为一个结点和多个结点。


//出队列
void  QueuePop(Queue* pq)
{
  assert(pq);
  //在删除数据之前需要考虑队列是否为空;
  assert(!pq->head==NULL);
    QNode* next = pq->head->next;//记录头结点后面的结点
    free(pq->head);//是否头节点
    pq->head = next;//将头指针重新指向后面的结点
    //注意如果最后head和tail指向同一个结点,也就是最后一个结点时,tail会变成野指针。
  //方法1:
  if (pq->head == NULL)
  {
    pq->tail = NULL;
  }
  pq->size--;
}


方法2:


void  QueuePop(Queue* pq)
{
  assert(pq);
  //在删除数据之前需要考虑队列是否为空;
  assert(!pq->head==NULL);
    //注意如果最后head和tail指向同一个结点,也就是最后一个结点时,tail会变成野指针。
  if (pq->head->next == NULL)
  {
    //直接释放最后一个结点
    free(pq->head);
     pq->head = pq->tail = NULL;
   }
  else
  {
    QNode* next = pq->head->next;//记录头结点后面的结点
    free(pq->head);//是否头节点
    pq->head = next;//将头指针重新指向后面的结点
  }
    pq->size--;
}


🕓4.获取队列头部元素


获取队头数据,就直接将头结点的数据拿走即可,不过在拿之前需要进行判断队列是否为空,如果为空,那还拿什么


//获取队头数据
QDatatype  QueueFront(Queue* pq)
{
  assert(pq);//断言判断是否传错
  assert(!QueueEmpty(pq));//判断是否为空
  return pq->head->data;//队头数据
}


🕔5.获取队列尾元素


和获取队头数据一样需要判断队列是否为空


//获取队尾数据
QDatatype  QueueBack(Queue* pq)
{
  assert(pq);//断言判断是否传错
  assert(!QueueEmpty(pq));//判断是否为空
  return pq->tail->data;//队尾数据
}


🕕6.获取队列中有效元素的个数


看,最开始的问题即将揭晓,在分装的结构体中加上size的作用是什么?


加上size后我们就可以快速获取队列中有效元素的个数了,如果没有加上,要获取个数,那必须对链表进行遍历,时间复杂度就是O(N),而现在就是O(1)。同样在单链表中我们如果想快速获得链表中有效数据的个数,也可以将节点与size分装成一新的结构体。那样就可以啦。


有的人想在带有哨兵头的链表中快速获取个数的办法是将size放入哨兵头里,想着反正哨兵头也不存储数据,就想着将size放进去,但是可以吗?


当然不可以,我们定义的链表中数据的类型有时是知道的,有时是不知道的,当数据类型是int类型是哨兵头里面可以放size,但当链表使用的数据类型是char? double 类型时,你放进去一个int类型的size进去?可以吗?


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


🕖7.检查队列是否为空


bool  QueueEmpty(Queue* pq)
{
  assert(pq);
  return pq->size == 0;
}


🕗8.销毁队列


与头删类似,就是循环删除。当删除为空时,停止


然后将数据都置0


//销毁队列
void  QueueDestroy(Queue* pq)
{
  assert(pq);
  QNode* cur = pq->head;
  while (cur)
  {
    QNode* next = cur->next;
    free(cur);
    cur = next;
  }
  //保存下一个释放,保存下一个释放
  pq->head = pq->tail = NULL;
  pq->size = 0;
}


相关文章
|
18天前
|
存储 Java
【数据结构】优先级队列(堆)从实现到应用详解
本文介绍了优先级队列的概念及其底层数据结构——堆。优先级队列根据元素的优先级而非插入顺序进行出队操作。JDK1.8中的`PriorityQueue`使用堆实现,堆分为大根堆和小根堆。大根堆中每个节点的值都不小于其子节点的值,小根堆则相反。文章详细讲解了如何通过数组模拟实现堆,并提供了创建、插入、删除以及获取堆顶元素的具体步骤。此外,还介绍了堆排序及解决Top K问题的应用,并展示了Java中`PriorityQueue`的基本用法和注意事项。
23 5
【数据结构】优先级队列(堆)从实现到应用详解
|
6天前
|
前端开发
07_用队列实现栈
07_用队列实现栈
|
6天前
|
测试技术
02_由两个栈组成的队列
02_由两个栈组成的队列
|
9天前
|
存储
|
26天前
|
存储 C语言
数据结构基础详解(C语言): 栈与队列的详解附完整代码
栈是一种仅允许在一端进行插入和删除操作的线性表,常用于解决括号匹配、函数调用等问题。栈分为顺序栈和链栈,顺序栈使用数组存储,链栈基于单链表实现。栈的主要操作包括初始化、销毁、入栈、出栈等。栈的应用广泛,如表达式求值、递归等场景。栈的顺序存储结构由数组和栈顶指针构成,链栈则基于单链表的头插法实现。
151 3
|
27天前
|
Java
【数据结构】栈和队列的深度探索,从实现到应用详解
本文介绍了栈和队列这两种数据结构。栈是一种后进先出(LIFO)的数据结构,元素只能从栈顶进行插入和删除。栈的基本操作包括压栈、出栈、获取栈顶元素、判断是否为空及获取栈的大小。栈可以通过数组或链表实现,并可用于将递归转化为循环。队列则是一种先进先出(FIFO)的数据结构,元素只能从队尾插入,从队首移除。队列的基本操作包括入队、出队、获取队首元素、判断是否为空及获取队列大小。队列可通过双向链表或数组实现。此外,双端队列(Deque)支持两端插入和删除元素,提供了更丰富的操作。
30 0
【数据结构】栈和队列的深度探索,从实现到应用详解
【数据结构】栈和队列
【数据结构】栈和队列
|
2月前
|
存储
【数据结构】栈和队列-->理解和实现(赋源码)
【数据结构】栈和队列-->理解和实现(赋源码)
27 5
|
3月前
|
存储 前端开发 DataX
【数据结构】栈和队列
数据结构中的栈和队列
34 1
【数据结构】栈和队列
|
2月前
|
机器学习/深度学习 消息中间件 缓存
栈与队列的实现
栈与队列的实现
41 0