欢迎各位大佬交流!
目录
[1、测试入队 + 出队 + 判空](#1、测试入队 + 出队 + 判空)
一、队列的概念及结构
1、队列的基本概念
队列是一种线性数据结构,遵循**先进先出(FIFO, First In First Out)**原则;
最早进入队列的元素最先被移除,类似于现实生活中的排队场景;
队列的操作通常限制在两端进行:元素从队尾(rear)入队,从队头(front)出队
2、队列的结构
队列的存储结构既可以用数组来实现,也可以用链表来实现;
注意:此处我们说的链表是单链表!不考虑双向链表是因为双向链表空间较大,且双向链表功能复杂,性能较低!
那么到底是用数组来实现栈?还是链表呢?
同样地,我们和分析栈的实现方式一样,通过一些操作的时间复杂度来来进行分析:
a、对于入队而言:如果是链表实现的队列,首先需要找到队尾元素,这样就需要O(N)级别的时间复杂度;
如果是数组实现的队列,我们可以判断完空间情况后直接利用 top 尾插;属于O(1)级别
b、对于出队而言;如果是链表实现的队列,更改释放完头节点后更改头节点即可;O(1)级别
如果是数组实现的队列,想要出队就需要从第二个节点开始均向前挪动一位,属于O(N)级别
显然:我们不想频繁地移动数据;
但是如果使用链表的话,入队又是O(N)级别的;该怎么优化呢?
为什么入队是O(N)级别的?是因为每次都需要遍历一遍队列才能找到最后一个节点,那不妨我们在设计参数时带上尾指针;
这样不就将O(N)降低到了O(1)级别吗?
这样的话我们在入队时传入的参数就有三个,分别是头指针,尾指针,变量x;
并且传入的还得是二级指针;这样就使得函数看起来很冗余!
能不能再优化一下?
如果我们定义一个结构体包含头尾指针会怎样?
这样每次传入只需要传入这个结构体的地址即可,并且用一级指针来接收完全可行;
这样就使得函数显得简洁、高效
综上:我们选择用链表来实现队列
二、代码实现
注意:我们是用链表来实现栈
说明:由于队列的实现较为简单,因此所有函数都封装好之后再一起测试
首先来完成准备工作:同样创建三个文件,Queue.c、Queue .h、test.c
接着定义出Queue结构体
cpp
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int QDatatype;
//定义Queue
typedef struct QListNode
{
struct QListNode* next;
QDatatype val;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
}Queue;
0、初始化
分析逻辑:
将Queue结构体中phead 和 ptail 置为空
cpp
//初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = pq->ptail = NULL;
//在获取队列中有效元素个数中说明size
pq->size = 0;
}
1、入队
分析逻辑:
首先申请节点空间,记得将 val 置为 x,next 置为空;
接着判断是否是入队的第一个元素;
如果是,就要修改头尾指针,否则就直接像链表尾插操作一样;
cpp
//入队
void QueuePush(Queue* pq, QDatatype x)
{
assert(pq);
assert(pq->phead);
//申请节点
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc failed!\n");
return;
}
newnode->val = x;
newnode->next = NULL;
if (pq->ptail == NULL)
{
//第一个入队元素
pq->phead = pq->ptail = newnode;
}
else
{
//直接在队尾插入
pq->ptail->next = newnode;
pq->ptail = newnode;
}
//在获取队列中有效元素个数中说明size
pq->size++;
}
2、出队
分析逻辑:
注意:出队是从头节点那一端出队
由链表中头删的经验可知无论是一个元素还是多个元素,删除逻辑都是一样的!
因此我们先暂存头节点的下一个节点next;
接着 free 头节点;
最终将 next 赋值给 头节点;
最后一个元素出队后,将尾指针置为空!
cpp
//出队
void QueuePop(Queue* pq)
{
assert(pq);
//队列不能为空
assert(pq->size > 0);
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
if (pq->phead == NULL)
{
pq->ptail = NULL;
}
//在获取队列中有效元素个数中说明size
pq->size--;
}
3、获取队头元素
分析逻辑:
直接返回头节点的 val 即可
cpp
//获取队头元素
QDatatype QueueFront(Queue* pq)
{
assert(pq);
assert(pq->phead);
return pq->phead->val;
}
4、获取队尾元素
分析逻辑:
直接返回尾节点的 val 即可
cpp
//获取队尾元素
QDatatype QueueBAck(Queue* pq)
{
assert(pq);
assert(pq->ptail);
return pq->ptail->val;
}
5、获取队列中有效元素个数
分析逻辑:
注意:不能直接用尾指针 - 头指针!两者并非顺序存储!
那难道还要遍历一遍队列吗?
我们直接在定义头尾指针的结构体中加入 size 变量;
用 size 统计元素个数
此时我们就需要更新我们上面的代码,更新 pq->size ;
cpp
//获取队列中有效元素个数
int QueueSize(Queue* pq)
{
//不能直接用尾指针 - 头指针
//两个指针并非顺序存储
assert(pq);
return pq->size;
}
6、检测队列是否为空
分析逻辑:
其实就是判断头指针或尾指针是否等于NULL
cpp
//检测队列是否为空
bool QueueEmpty(Queue* pq)
{
assert(pq);
//return pq->phead == NULL;
//在获取队列中有效元素个数中说明size
return pq->size == 0;
}
7、销毁队列
分析逻辑:
按照顺序从头到尾遍历一遍,按顺序销毁即可!
最终在执行完函数之后记得显式将结构体指针置为空!
因为传入的是一级结构体指针,无法修改指针本身
cpp
//销毁队列
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->phead;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
三、测试代码
1、测试入队 + 出队 + 判空

2、测试获取队头元素

3、测试获取队尾元素

如有不足之处恳请指出!!!