文章目录
1. 队列的概念和结构
队列(Queue)是一种特殊的线性表,它具有先进先出(FIFO, First In First Out)的特性。在队列中,数据的插入操作被称为入队(Enqueue),数据元素的插入只能在队列的一端进行,这一端通常被称为队尾(Rear);相应地,数据的删除操作被称为出队(Dequeue),数据元素的删除只能在队列的另一端进行,这一端被称为队首(Front)。
队列的存储结构主要分为两种:链式存储和顺序存储。下面来逐一实现
2. 队列的链式存储实现
队列的链式存储结构为单链表,在链式存储队列中,因为将数据入队是从队尾插入所以可能需要从头节点开始遍历到尾节点,时间复杂度较高,为O(n)。但为了降低时间复杂度可以额外开辟一个结构体来存储队列的头指针和尾指针,这样可以有效降低时间复杂度。此外,如果想要知道队列里有效数据的个数,可以往队列结构体里再存储一个size变量记录队列中有效数据的个数
对单链表的实现不了解的可以去看一下我之前的博客:数据结构—单链表
下面我们来定义一下队列的链式存储结构和队列具体要实现哪些功能:
//定义队列结点结构
typedef int QDataType;
typedef struct QueueNode {
QDataType data;//存储数据
struct QueueNode* next;//存储指向下一个结点的指针
}QueueNode;
//定义队列结构
typedef struct Queue {
QueueNode* phead;//头结点指针
QueueNode* ptail;//尾结点指针
int size;//队列中有效数据的个数
}Queue;
//初始化
void QueueInit(Queue* pq);
//入队列,队尾
void QueuePush(Queue* pq, QDataType x);
//出队列,队头
void QueuePop(Queue* pq);
//队列判空
bool QueueEmpty(Queue* pq);
//取队头数据
QDataType QueueFront(Queue* pq);
//取队尾数据
QDataType QueueBack(Queue* pq);
//队列有效数据的个数
int Queuesize(Queue* pq);
//销毁队列
void QueueDestory(Queue* pq);
//打印队列数据
void QueuePrint(Queue* pq);
2.1 初始化
思路:将队列这个结构体变量的地址传过来用一级指针来接收,在函数内部将队列的头尾指针指向空和有效数据个数置为0即可,这样就实现了形参改变实参。
//初始化
void QueueInit(Queue* pq)
{
assert(pq);//pq!=NULL
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
2.2 判断队列是否为空
思路:如果队列的头指针或者尾指针指向空,则说明队列为空
//队列判空
bool QueueEmpty(Queue* pq)
{
assert(pq);//pq!=NULL
return pq->phead == NULL && pq->ptail == NULL;
}
2.3 入队列
思路:首先申请一个新节点空间newnode,如果申请失败就打印错误信息,再异常退出,如果申请成功就将数据存储在新节点中,再将新节点的next指针指向空。随后再判断队列是否为空,如果为空就将新节点空间newnode赋给头结点和尾节点的指针,如果不为空,就先将尾节点的next指针指向新节点newnode,再更新尾节点指针(将尾节点指针指向新节点newnode),最后不要忘了将队列中的有效数据个数加一
//入队列,队尾
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);//pq!=NULL
//申请新节点
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)
{
//申请失败
perror("malloc fail!");//打印提升消息
exit(1);//异常退出
}
newnode->data = x;
newnode->next = NULL;
//申请成功,判断队列是否为空
if (QueueEmpty(pq))
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
2.4 出队列
思路:首先队列不能为空,队列为空直接报错。如果队列不为空,则分以下两种情况:
第一种情况:如果队列只有一个节点,那么头尾指针都指向这个节点,所以直接销毁这个节点的空间后再将头尾指针指向空(否则头尾指针会变成野指针)
第二种情况:如果队列的节点个数大于1的话,因为出队列是从队头出,所以可以先将头指针赋给del,再将头指针指向头指针指向的下一个节点的指针(pq->phead = pq->phead->next),再将del指向的空间销毁并且将del指向空。同理,也可以将头指针的next指针用新指针存储起来,再销毁头指针的空间并且将头指针指向新指针,两种方法都可以,以下用的是第一种。
注意:因为是出队列,最后不要忘了将队列中的有效数据个数size减一
//出队列,队头
void QueuePop(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不能为空
if (pq->phead==pq->ptail)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else
{
QueueNode* del = pq->phead;
pq->phead = pq->phead->next;
free(del);
del = NULL;
}
pq->size--;
}
2.5 取队头数据
思路:因为是取队头数据,所以队列不能为空,直接将队头数据返回即可。
//取队头数据
QDataType QueueFront(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不能为空
return pq->phead->data;
}
2.6 取队尾数据
思路:因为是取队尾数据,所以队列不能为空,直接将队尾数据返回即可。
//取队尾数据
QDataType QueueBack(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不能为空
return pq->ptail->data;
}
2.7 队列有效数据的个数
思路:直接将队列中有效数据的个数size返回即可。
//队列有效数据的个数
int Queuesize(Queue* pq)
{
assert(pq);//pq!=NULL
return pq->size;
}
2.8 打印队列数据
思路:首先队列不能为空,只需每次打印队头数据,再出队列即可,当队列为空时就停止循环,所以循环条件为队列不为空:!QueueEmpty(pq)
//打印队列数据
void QueuePrint(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不能为空
while (!QueueEmpty(pq))
{
printf("%d ", QueueFront(pq));
QueuePop(pq);
}
printf("\n");
}
2.9 销毁
思路:首先队列不能为空,定义一个节点指针pcur指向队列的头结点,只需将头指针赋给pcur即可(pcur=pq->phead),然后从头节点开始遍历队列,再定义一个指针指向释放节点的下一个节点(next),每次释放完当前节点pcur后就让pcur指向下一个节点next,直到全部节点都释放完,最后再将队列的头指针phead和尾指针ptail指向空,还有队列中的有效数据个数size置为0即可。
//销毁队列
void QueueDestory(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不能为空
QueueNode* pcur = pq->phead;
while (pcur)
{
QueueNode* next = pcur->next;
free(pcur);
pcur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
2.10 源代码
Queue.h头文件
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//定义队列结点结构
typedef int QDataType;
typedef struct QueueNode {
QDataType data;//存储数据
struct QueueNode* next;//存储指向下一个结点的指针
}QueueNode;
//定义队列结构
typedef struct Queue {
QueueNode* phead;//头结点指针
QueueNode* ptail;//尾结点指针
int size;//队列中有效数据的个数
}Queue;
//初始化
void QueueInit(Queue* pq);
//入队列,队尾
void QueuePush(Queue* pq, QDataType x);
//出队列,队头
void QueuePop(Queue* pq);
//队列判空
bool QueueEmpty(Queue* pq);
//取队头数据
QDataType QueueFront(Queue* pq);
//取队尾数据
QDataType QueueBack(Queue* pq);
//队列有效数据的个数
int Queuesize(Queue* pq);
//销毁队列
void QueueDestory(Queue* pq);
//打印队列数据
void QueuePrint(Queue* pq);
Queue.c源文件
#include "Queue.h"
//初始化
void QueueInit(Queue* pq)
{
assert(pq);//pq!=NULL
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
//队列判空
bool QueueEmpty(Queue* pq)
{
assert(pq);//pq!=NULL
return pq->phead == NULL && pq->ptail == NULL;
}
//入队列,队尾
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);//pq!=NULL
//申请新节点
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)
{
//申请失败
perror("malloc fail!");//打印提升消息
exit(1);//异常退出
}
newnode->data = x;
newnode->next = NULL;
//申请成功,判断队列是否为空
if (QueueEmpty(pq))
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
//出队列,队头
void QueuePop(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不能为空
if (pq->phead==pq->ptail)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else
{
QueueNode* del = pq->phead;
pq->phead = pq->phead->next;
free(del);
del = NULL;
}
pq->size--;
}
//取队头数据
QDataType QueueFront(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不能为空
return pq->phead->data;
}
//取队尾数据
QDataType QueueBack(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不能为空
return pq->ptail->data;
}
//队列有效数据的个数
int Queuesize(Queue* pq)
{
assert(pq);//pq!=NULL
return pq->size;
}
//销毁队列
void QueueDestory(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不能为空
QueueNode* pcur = pq->phead;
while (pcur)
{
QueueNode* next = pcur->next;
free(pcur);
pcur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
//打印队列数据
void QueuePrint(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不能为空
while (!QueueEmpty(pq))
{
printf("%d ", QueueFront(pq));
QueuePop(pq);
}
printf("\n");
}
3. 队列的顺序存储实现(循环队列)
对顺序表实现不熟悉的可以看一下我之前的博客:数据结构—顺序表
队列的顺序存储结构通常是由一个静态数组和一个记录队列头元素位置的变量front以及一个记录队列尾元素的下一个位置的变量rear组成,因为数组的空间大小在创建时就已经确立了,所以顺序存储结构还存储了队列的最大容量maxsize
假设一开始我们将front和rear两个变量置为0,每次将元素入队后rear都会加一,那么当rear=maxsize时就可以判断队列为满吗?显然我们忽略了一个重要的东西:front一定为0吗?,如果我们一开始将几个元素入队(入队在队尾入),然后再出队(出队是在队头出)那么front就会++,不会再指向0,然后再往队列里面插入元素,当rear=maxsize时如果还有元素要插入的话就会造成数组越界(从下标为0开始,最大下标为maxsize-1),但是front(front不为0)之前还有位置是空的还可以再插入数据,此时怎么将数据插入到下标为0的位置呢?
我们可以用pq->rear = (pq->rear + 1) % pq->maxsize这个式子,当rear==maxsize-1时执行这条语句后rear就会变成0,也就是说将数组最大下标为maxsize-1的位置插入数据后,下一个要插入数据的位置就变成了0,这样就完美解决了数组越界问题,队列也变成了循环队列。同理,每次出队时front都会加一,当从数组最大下标位置(maxsize-1)出队后front就会回到0这个位置,可以推出式子为:pq->front = (pq->front + 1) % pq->maxsize
如上图,那么又来了一个问题:当rear=front时(初始化都为0)可以判断队列为空,但是当队列为满时rear也刚好等于front,那该怎么解决这个问题呢?以下给出三种解决方案:
第一种:人为浪费一个空间,在申请数组空间大小时再多申请一个空间,然后当(pq->rear + 1)%(pq->maxsize + 1)等于pq->front时队列为满
第二种:在队列结构体里再存储一个记录队列有效数据个数的变量size,当size=maxsize时说明队列为满
第三种:在对列结构体里再存储一个变量msg,初始化为0,每次出队都将msg设置为0,每次入队都将msg设置为1。因为只有出队才能让队列为空,只有入队才能让队列为满。当pq->rear等于pq->front同时msg等于1时队列为满,当pq->rear等于pq->front同时msg等于0时队列为空
接下来介绍第一种方法,第一种方法示意图如下:
首先,定义队列的顺序存储结构体和具体要实现的功能:
//定义队列结构
typedef int QDataType;
typedef struct Queue {
QDataType* data;//存储有效数据的数组
int front;//队列头指针
int rear;//队列尾指针(指向尾元素的下一个位置)
int maxsize;//队列最大容量
}Queue;
//初始化
Queue* QueueInit(int sum);
//队列判空
bool QueueEmpty(Queue* pq);
//判断队列是否为满
bool QueueFull(Queue* pq);
//入队列,队尾
void QueuePush(Queue* pq, QDataType x);
//出队列,队头
void QueuePop(Queue* pq);
//取队头数据
QDataType QueueFront(Queue* pq);
//取队尾数据
QDataType QueueBack(Queue* pq);
//队列有效数据的个数
int Queuesize(Queue* pq);
//销毁队列
void QueueDestory(Queue* pq);
//打印队列数据
void QueuePrint(Queue* pq);
3.1 初始化
思路:首先创建一个队列结构体空间,最后再返回这个队列结构体指针,开辟大小为sum+1的数组空间,申请失败就打印错误信息并退出,申请成功就将数组空间赋给pq->data,并且将front和rear置为0,再将sum赋给maxsize。
//初始化
Queue* QueueInit(int sum)
{
Queue* pq = (Queue*)malloc(sizeof(Queue));//创建一个队列空间
QDataType* tmp = (QDataType*)malloc((sum + 1) * sizeof(QDataType));
//申请sum+1个空间
if (tmp == NULL)
{
//申请失败
perror("malloc fail!");
exit(1);
}
//申请成功
pq->data = tmp;
pq->front = pq->rear = 0;
pq->maxsize = sum;
return pq;
}
3.2 判断队列是否为空
思路:当pq->rear等于pq->front时为空
//队列判空
bool QueueEmpty(Queue* pq)
{
assert(pq);//pq!=NULL
return pq->front == pq->rear;
}
3.3 判断队列是否为满
思路:当(pq->rear + 1) % (pq->maxsize + 1) 等于 pq->front时为满
//判断队列是否为满
bool QueueFull(Queue* pq)
{
assert(pq);//pq!=NULL
return (pq->rear + 1) % (pq->maxsize + 1) == pq->front;
}
3.4 入队列
思路:既然要入队列,所以队列不能为满,要使用assert断言。每次往数组下标为rear的位置插入数据后,都不要忘了将rear先+1后取模,防止数组越界
//入队列,队尾
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);//pq!=NULL
assert(!QueueFull(pq));//队列不为满
pq->data[pq->rear] = x;
pq->rear = (pq->rear + 1) % (pq->maxsize + 1);//因为申请了maxsize+1个空间所以被取余的是pq->maxsize+1
}
3.5 出队列
思路:既然要出队列,所以队列不能为空,要使用assert断言。每次将数组下标为front的数据出队列后,为了防止数组越界,所以最后不要忘了将front先+1后取模
//出队列,队头
void QueuePop(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不为空
pq->front = (pq->front + 1) % (pq->maxsize + 1);
}
3.6 取队头数据
思路:直接将队头数据返回即可
//取队头数据
QDataType QueueFront(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不为空
return pq->data[pq->front];
}
3.7 取队尾数据
思路:先定义一个变量prev等于rear-1(因为rear-1刚好是队尾数据的下标)。因为rear可能指向0,再减一就变成-1了,所以还得先判断rear是否等于0。如果等于0的话,那么队尾数据的下标刚好等于maxsize,所以将maxsize赋给prev,最后将队尾数据返回即可
//取队尾数据
QDataType QueueBack(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不为空
int prev = pq->rear - 1;
if (pq->rear == 0)
{
prev = pq->maxsize;
}
return pq->data[prev];
}
3.8 队列有效数据的个数
思路:直接套用公式(rear-front+maxsize)%maxsize即可
//队列有效数据的个数
int Queuesize(Queue* pq)
{
assert(pq);//pq!=NULL
return (pq->rear - pq->front + pq->maxsize) % pq->maxsize;
}
3.9 打印队列数据
思路:打印完队头数据就将队头出队,一直到队列为空即可,所以循环条件为:!QueueEmpty(pq)
//打印队列数据
void QueuePrint(Queue* pq)
{
assert(pq);//pq!=NULL
while (!QueueEmpty(pq))
{
printf("%d ", QueueFront(pq));
QueuePop(pq);
}
printf("\n");
}
3.10 销毁
思路:如果pq->data不为空则销毁数组空间并且将front,rear,maxsize置为0即可,如果pq->data为空指针(指向的数组为空)则直接将front,rear,maxsize置为0即可
//销毁队列
void QueueDestory(Queue* pq)
{
assert(pq);//pq!=NULL
if (pq->data)
{
free(pq->data);//销毁空间
}
pq->front = pq->rear = pq->maxsize = 0;
}
3.11 源代码
Queue.h头文件
#pragma once//只包含一次头文件,防止重复调用
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//定义队列结构
typedef int QDataType;
typedef struct Queue {
QDataType* data;//存储有效数据的数组
int front;//队列头指针
int rear;//队列尾指针(指向尾元素的下一个位置)
int maxsize;//队列最大容量
}Queue;
//初始化
Queue* QueueInit(int sum);
//队列判空
bool QueueEmpty(Queue* pq);
//判断队列是否为满
bool QueueFull(Queue* pq);
//入队列,队尾
void QueuePush(Queue* pq, QDataType x);
//出队列,队头
void QueuePop(Queue* pq);
//取队头数据
QDataType QueueFront(Queue* pq);
//取队尾数据
QDataType QueueBack(Queue* pq);
//队列有效数据的个数
int Queuesize(Queue* pq);
//销毁队列
void QueueDestory(Queue* pq);
//打印队列数据
void QueuePrint(Queue* pq);
Queue.c源文件
//初始化
Queue* QueueInit(int sum)
{
Queue* pq = (Queue*)malloc(sizeof(Queue));//创建一个队列空间
QDataType* tmp = (QDataType*)malloc((sum + 1) * sizeof(QDataType));
//申请sum+1个空间
if (tmp == NULL)
{
//申请失败
perror("malloc fail!");
exit(1);
}
//申请成功
pq->data = tmp;
pq->front = pq->rear = 0;
pq->maxsize = sum;
return pq;
}
//队列判空
bool QueueEmpty(Queue* pq)
{
assert(pq);//pq!=NULL
return pq->front == pq->rear;
}
//判断队列是否为满
bool QueueFull(Queue* pq)
{
assert(pq);//pq!=NULL
return (pq->rear + 1) % (pq->maxsize + 1) == pq->front;
}
//入队列,队尾
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);//pq!=NULL
assert(!QueueFull(pq));//队列不为满
pq->data[pq->rear] = x;
pq->rear = (pq->rear + 1) % (pq->maxsize + 1);//因为申请了maxsize+1个空间所以被取余的是pq->maxsize+1
}
//出队列,队头
void QueuePop(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不为空
pq->front = (pq->front + 1) % (pq->maxsize + 1);
}
//取队头数据
QDataType QueueFront(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不为空
return pq->data[pq->front];
}
//取队尾数据
QDataType QueueBack(Queue* pq)
{
assert(pq);//pq!=NULL
assert(!QueueEmpty(pq));//队列不为空
int prev = pq->rear - 1;
if (pq->rear == 0)
{
prev = pq->maxsize;
}
return pq->data[prev];
}
//队列有效数据的个数
int Queuesize(Queue* pq)
{
assert(pq);//pq!=NULL
return (pq->rear - pq->front + pq->maxsize) % pq->maxsize;
}
//打印队列数据
void QueuePrint(Queue* pq)
{
assert(pq);//pq!=NULL
while (!QueueEmpty(pq))
{
printf("%d ", QueueFront(pq));
QueuePop(pq);
}
printf("\n");
}
//销毁队列
void QueueDestory(Queue* pq)
{
assert(pq);//pq!=NULL
if (pq->data)
{
free(pq->data);//销毁空间
}
pq->front = pq->rear = pq->maxsize = 0;
}
4. 队列的顺序存储和链式存储的优缺点
一. 顺序存储队列:
优点:
空间利用率高:在静态分配的内存空间中,顺序存储队列的存储密度大,空间利用率高。
访问效率高:由于数据是连续存储的,队列的访问(如查看队首或队尾元素)操作效率高,时间复杂度为O(1)。
缺点:
假溢出问题:在顺序存储队列中,如果队列满了(即队尾指针达到了数组的上界),但实际可能队首还有空间未被利用,这会导致队列出现“假溢出”现象。解决假溢出的方法之一是采用循环队列,但这会增加实现的复杂度。
动态扩容成本高:如果采用动态数组实现顺序队列,当队列容量不足时,需要进行扩容操作,这通常涉及到大量数据的移动,成本较高。
空间限制:顺序存储队列的大小受限于静态分配的内存大小,或者动态扩容的阈值。
二. 链式存储队列:
优点:
无假溢出问题:链式存储队列通过指针连接节点,理论上队列的大小只受限于系统内存的大小,不存在假溢出问题。
动态扩容方便:在链式存储队列中,当需要增加新的元素时,只需动态地分配新的节点即可,无需进行大量数据的移动。
空间利用率灵活:每个节点可以只存储必要的数据和指针,空间利用率较为灵活。
缺点:
空间利用率相对较低:由于每个节点除了存储数据外,还需要存储指针,因此相比于顺序存储,链式存储的空间利用率较低。
访问效率低:在链式存储队列中,访问某个特定位置的元素可能需要从头节点开始遍历,时间复杂度较高,为O(n)。但为了降低时间复杂度可以额外开辟一个结构体来存储队列的头指针和尾指针,这样可以有效降低时间复杂度,变为O(1),但同时也提高了内存开销。
内存管理开销:动态分配和释放节点需要额外的内存管理开销。
对以上内容有不同看法的欢迎来讨论,希望对大家的学习有帮助,多多支持哦!