上集回顾:循环队列
第一集:顺序存储队列
第二集:链式存储队列
第三集:线性池中的任务队列
第四集:循环队列
观看本系列博文提醒:
你将学会队列的两种最基本的表现形式:顺序存储队列 和 链式存储队列;
一个扩展队列的使用方法:循环队列;
两个企业级队列的应用:线性池中的任务队列 和 优先链式存储队列。
队列的原理
队列是一种受限的线性表,(Queue),它是一种运算受限的线性表,先进先出(FIFO First In First Out).
例如上图中,圆球1先进,也是圆球1先出。
队列是一种受限的线性结构
- 它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。
- 生活中队列场景随处可见: 比如在电影院, 商场, 或者厕所排队。。。。。。
由上图我们可以知道,队列中有两个“指针”,front指向队首,rear指向队尾;
至于length,他是整个队列的总长度(因为一个队列他是有固定长度的)。
优先链式存储队列
英雄联盟游戏里面防御塔都有一个自动攻击功能,小兵排着队进入防御塔的攻击范围,防御塔先攻击靠得最近的小兵,这时候大炮车的优先级更高(因为系统判定大炮车对于防御塔的威胁更大),所以防御塔会优先攻击大炮车。而当大炮车阵亡,剩下的全部都是普通小兵,这时候离得近的优先级越高,防御塔优先攻击距离更近的小兵。
优先队列: 它的入队顺序没有变化,但是出队的顺序是根据优先级的高低来决定的。优先级高的优先出队。
以上笔记来自腾讯课堂骑牛学院!
讲简单一点,优先先队列就是和链式存储队列是一样的,只是他的出队方式有点不一样而已,优先队列出队需要根据优先级。优先级大的先出队,优先级小的后出队。
就好比如一行人在银行排队取钱,突然间,来了一位银行的vip客户,那么,该vip客户就不需要排队,可以优先进行处理,优先取钱。
那么我们该如何定义优先队列呢?
很简单,和第二集顺序储队列差不多,只是优先队列还多了一个优先级的变量。
那么我们该如何出队优先队列呢?
这才是我们的重点,这里涉及到企业级的高逼格代码,在下面下边我会详细说明。
其他的操作都是和第二集中的链式存储队列一模一样。
定义
#define MaxSize 5 // 队列的最大容量
typedef int DateType; // 队列中元素的类型
typedef struct _QNode { // 节点结构
int priority; // 优先级别
DateType date; // 元素
struct _QNode* next;
}QNode;
typedef QNode* QueuePar;
typedef struct Queue {
int lenght; // 队列的长度
QueuePar front; // 队头指针
QueuePar rear; // 队尾指针
}LinkQueue;
先定义节点,再根据节点定义队列!
初始化
// 初始化
bool inItLinkQueue(LinkQueue*& LQ) {
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
LQ->front = LQ->rear = NULL;
LQ->lenght = 0;
return true;
}
初始化队列就是头指针front和尾指针rear都是指向NULL。而且他的长度length等于0.
判断是否为空
// 判断是否为空
bool estimateLinkQueueEmpty(LinkQueue*& LQ) {
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
if (LQ->front == NULL) {
return true;
}
return false;
}
判断条件也很简单,只需要判断头指针是否指向NULL就可以了。如上图。
因为空队列头指针front和尾指针rear都是指向NULL。而且他的长度length等于0.
判断是否已满
// 判断是否已满
bool estimateLinkQueuefull(LinkQueue*& LQ) {
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
if (LQ->lenght == MaxSize) {
return true;
}
return false;
}
队列中有一个变量时用于统计队列的元素个数的,只需要将他与队列的存储仅限长度作比较,就可以得出队列是否已满!
入队,将元素插入队列中
// 入队,将元素插入队列中
bool LinkQueueInsertValue(LinkQueue*& LQ, int date, int priority) { // 参数二:插入的元素;参数三:插入的优先等级
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
if (estimateLinkQueuefull(LQ)) {
cout << "队列已满!" << endl;
return false;
}
QNode* QN = new QNode;
QN->date = date;
QN->priority = priority;
QN->next = NULL;
if (estimateLinkQueueEmpty(LQ)) {
LQ->front = LQ->rear = QN; // 队首指针与队尾指针都要指向新插入的节点
} else {
LQ->rear->next = QN; // 在队尾插入节点(旧队尾节点和新队尾节点连起来)
LQ->rear = QN;
}
LQ->lenght += 1;
return true;
}
入队他有两种情况:
-
队列为空
因为我们传入的是待插入的元素,所以我们先创建一个新的节点。
队列插入元素后,队首指针和队尾指针都要指向第一个元素(如上图)。
然后length自增一。 -
队列不为空
因为我们传入的是待插入的元素,所以我们先创建一个新的节点.
头指针不需要动。尾指针rear的next需要指向新插入的节点,然后尾节点在指向新插入的节点。
然后length自增一。
请注意理解好:插入前尾节点rear的next是指向NULL的,所以得将next指向新插入的节点(将两个节点链起来),然后再将尾节点指向新插入的节点。
这两个步骤弄完后,rear有从新指向新插入的尾节点,那么他的next就为NULL。
出队
// 出队
bool deleteLinkQueue(LinkQueue*& LQ, DateType* date) { // 参数二:保存出队的元素返回
QNode** rear = NULL; // 指向待出队节点
QNode* rear_node = NULL; // 指向待出队节点的前一个节点
QNode* last = NULL; // 指向循环遍历当前节点的前一个节点
QNode* tem = NULL; // 循环遍历
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
if (estimateLinkQueueEmpty(LQ)) {
cout << "队列为空!" << endl;
return false;
}
rear = &(LQ->front); // 赋值第一个节点的地址
last = LQ->front; // 指向第一个节点
tem = last->next; // 指向第一个节点的下一个节点
while (tem) {
if (tem->priority > (*rear)->priority) {
rear = &(last->next); // 重新把优先级高的节点赋值给rear
rear_node = last; // rear_node指向优先级高的节点的前一个节点
}
last = tem; // 跟随tem遍历,指向tem的前一个节点
tem = tem->next; // 遍历
}
*date = (*rear)->date;
tem = (*rear); // 待出队节点赋值给tem,好释放内存
*rear = (*rear)->next; // 把待出队节点的前一个节点的next指向待出队节点的下一个节点
delete tem;
LQ->lenght -= 1; // 最后将length减一
//接下来存在 2 种情况需要分别对待
//1.删除的是首节点,而且队列长度为零
if (LQ->lenght == 0) {
LQ->rear = NULL;
}
//2.删除的是尾部节点
if (rear_node && rear_node->next == NULL) {
LQ->rear = rear_node;
}
return true;
}
删除一共有三种情况:
- 删除的是中间的某个优先级最高的节点(如上图);
- 删除的是队列只剩下的唯一一个节点(如下图一);
- 删除的是队列最后一个节点(如下图二)。
不管有多少种情况,我们都可以用一种代码搞定!
- 找到优先级最高的那个节点;
代码中,我们定义了四个临时变量:
QNode** rear = NULL; // 指向待出队节点
QNode* rear_node = NULL; // 指向待出队节点的前一个节点
QNode* last = NULL; // 指向循环遍历当前节点的前一个节点
QNode* tem = NULL; // 循环遍历
他们都有各自的作用。请看注释。
其中,last永远都是指向tem的前一个节点!
然后各司其职:
rear = &(LQ->front); // 赋值第一个节点的地址
last = LQ->front; // 指向第一个节点
tem = last->next; // 指向第一个节点的下一个节点
tem用于循环条件,因为已经默认第一个节点是优先级最高的节点了,所以tem指向第二个节点,而last就指向循环节点的前一个节点,也就是第一个节点。
循环开始:
while (tem) {
if (tem->priority > (*rear)->priority) {
rear = &(last->next); // 重新把优先级高的节点赋值给rear
rear_node = last; // rear_node指向优先级高的节点的前一个节点
}
last = tem; // 跟随tem遍历,指向tem的前一个节点
tem = tem->next; // 遍历
}
循环中我们用rear与循环的节点tem进行判断,rear是否是优先级最高的节点,如果不是,则需要将该节点更新rear(因为last是指向循环节点的前一个节点,所以可以用它的next的地址赋值给rear)。rear_node需要指向待删除节点的前一个节点,所以将last赋值给它。
if条件结束后,再将循环节点tem赋值给last,然后tem指向自己的下一个节点。直到tem等于NULL,循环结束。
当循环结束后:
rear指向了优先级最高的节点的地址;
rear_node指向了优先级最高节点的前一个节点;
last指向最后一个节点的位置;
tem指向NULL。
如下图:
- 进行出队操作:
tem = (*rear); // 待出队节点赋值给tem,好释放内存
*rear = (*rear)->next; // 把待出队节点的前一个节点的next指向待出队节点的下一个节点
delete tem;
LQ->lenght -= 1; // 最后将length减一
tem需要指向优先级最高的节点(用于释放);
而*rear = (*rear)->next;
是什么意思呢?
解析:
因为他的前一个节点是指向他的,所以*rear将自己的下一个节点赋值给自己,也就间接的把以自己为中心,将自己前后两个节点链起来,自己与他们断开。(如下图)
这里需要好好理解,小编当时在这里也卡了很久!这也是这套算法的绝妙之处,只有大牛才会这么写,因为这样的效率是很高的,这些代码也是一位大牛所教,所写的!
最后再将tem释放掉,也就是将优先级最高的节点的内存释放掉!
队列的长度也要减一。
-
接下来就可以判断剩下的两种情况了:
- 删除的是首节点,而且队列长度为零
if (LQ->lenght == 0) { LQ->rear = NULL; }
如果队列的长度为零的话,说明删除的是队列中唯一的节点,即队列中已 经没有任何节点了。所以我们我要将队尾指针rear指向NULL。
- 删除的是尾部节点
if (rear_node && rear_node->next == NULL) { LQ->rear = rear_node; }
现在rear_node的作用来了,因为他是指向最高优先级节点的前一个节点,即如果它的自身不为NULL而且它的next指针等于NULL的话,说明rear_node是指向最后一个节点的位置,即出队的是最后一个节点,导致rear_node变为最后一个节点了。
因为尾指针还是指向以前那个被释放掉的尾节点,所以尾指针rear需要指向新的尾节点,即LQ->rear = rear_node;
好了,优先级最高的节点出队的教程已经完了,这一点很难,很绕脑,不懂的朋友可以回头去看多两次,自己慢慢琢磨一下,想你一定会懂的!
输出队列中的元素
// 输出队列中的元素
bool linkQueuePrint(LinkQueue*& LQ) {
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
if (estimateLinkQueueEmpty(LQ)) {
cout << "链表为空!输出失败!" << endl;
return false;
}
QNode* tem = LQ->front; // 定义临时节点指向队首指针
while (tem) {
cout << tem->date << "[" << tem->priority << "]" << "\t";
tem = tem->next;
}
cout << endl;
return true;
}
定义临时节点指向头节点,使用临时节点遍历队列,将队列中的所有元素都输出。
清空队列
// 清空队列
bool clearLinkQueue(LinkQueue*& LQ) {
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
QNode* tem = LQ->front;
while (tem) {
tem = tem->next;
delete LQ->front;
LQ->front = tem;
}
LQ->front = LQ->rear = NULL;
LQ->lenght = 0;
return true;
}
定义临时节点指向队首指针,while循环遍历队列;
- 临时节点首先指向自己的下一个节点;
- 释放第一个节点;
- 再将临时节点赋值给释放掉的空节点;
- while循环结束后,还得将队首指针和队尾指针都指向NULL;
- 最后再将length赋值0.
测试代码:
#include <iostream>
#include <Windows.h>
using namespace std;
#define MaxSize 5 // 队列的最大容量
typedef int DateType; // 队列中元素的类型
typedef struct _QNode { // 节点结构
int priority; // 优先级别
DateType date; // 元素
struct _QNode* next;
}QNode;
typedef QNode* QueuePar;
typedef struct Queue {
int lenght; // 队列的长度
QueuePar front; // 队头指针
QueuePar rear; // 队尾指针
}LinkQueue;
// 初始化
bool inItLinkQueue(LinkQueue*& LQ) {
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
LQ->front = LQ->rear = NULL;
LQ->lenght = 0;
return true;
}
// 判断是否为空
bool estimateLinkQueueEmpty(LinkQueue*& LQ) {
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
if (LQ->front == NULL) {
return true;
}
return false;
}
// 判断是否已满
bool estimateLinkQueuefull(LinkQueue*& LQ) {
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
if (LQ->lenght == MaxSize) {
return true;
}
return false;
}
// 入队,将元素插入队列中
bool LinkQueueInsertValue(LinkQueue*& LQ, int date, int priority) { // 参数二:插入的元素;参数三:插入的优先等级
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
if (estimateLinkQueuefull(LQ)) {
cout << "队列已满!" << endl;
return false;
}
QNode* QN = new QNode;
QN->date = date;
QN->priority = priority;
QN->next = NULL;
if (estimateLinkQueueEmpty(LQ)) {
LQ->front = LQ->rear = QN; // 队首指针与队尾指针都要指向新插入的节点
} else {
LQ->rear->next = QN; // 在队尾插入节点(旧队尾节点和新队尾节点连起来)
LQ->rear = QN;
}
LQ->lenght += 1;
return true;
}
// 出队
bool deleteLinkQueue(LinkQueue*& LQ, DateType* date) { // 参数二:保存出队的元素返回
QNode** rear = NULL; // 指向待出队节点
QNode* rear_node = NULL; // 指向待出队节点的前一个节点
QNode* last = NULL; // 指向循环遍历当前节点的前一个节点
QNode* tem = NULL; // 循环遍历
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
if (estimateLinkQueueEmpty(LQ)) {
cout << "队列为空!" << endl;
return false;
}
rear = &(LQ->front); // 赋值第一个节点的地址
last = LQ->front; // 指向第一个节点
tem = last->next; // 指向第一个节点的下一个节点
while (tem) {
if (tem->priority > (*rear)->priority) {
rear = &(last->next); // 重新把优先级高的节点赋值给rear
rear_node = last; // rear_node指向优先级高的节点的前一个节点
}
last = tem; // 跟随tem遍历,指向tem的前一个节点
tem = tem->next; // 遍历
}
*date = (*rear)->date;
tem = (*rear); // 待出队节点赋值给tem,好释放内存
*rear = (*rear)->next; // 把待出队节点的前一个节点的next指向待出队节点的下一个节点
delete tem;
LQ->lenght -= 1; // 最后将length减一
//接下来存在 2 种情况需要分别对待
//1.删除的是首节点,而且队列长度为零
if (LQ->lenght == 0) {
LQ->rear = NULL;
}
//2.删除的是尾部节点
if (rear_node && rear_node->next == NULL) {
LQ->rear = rear_node;
}
return true;
}
// 输出队列中的元素
bool linkQueuePrint(LinkQueue*& LQ) {
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
if (estimateLinkQueueEmpty(LQ)) {
cout << "链表为空!输出失败!" << endl;
return false;
}
QNode* tem = LQ->front; // 定义临时节点指向队首指针
while (tem) {
cout << tem->date << "[" << tem->priority << "]" << "\t";
tem = tem->next;
}
cout << endl;
return true;
}
// 清空队列
bool clearLinkQueue(LinkQueue*& LQ) {
if (!LQ) {
cout << "队列不存在!" << endl;
return false;
}
QNode* tem = LQ->front;
while (tem) {
tem = tem->next;
delete LQ->front;
LQ->front = tem;
}
LQ->front = LQ->rear = NULL;
LQ->lenght = 0;
return true;
}
int main(void) {
LinkQueue* LQ = new LinkQueue;
DateType date = 0;
inItLinkQueue(LQ);
for (int i = 0; i < 5; i++) {
LinkQueueInsertValue(LQ, i * 5, i);
}
linkQueuePrint(LQ);
for (int i = 0; i < 5; i++) {
if (deleteLinkQueue(LQ, &date)) {
cout << "出队成功,出队的元素是:" << date << endl;
}
}
clearLinkQueue(LQ);
system("pause");
return 0;
}
运行截图:
总结:
优先队列最难的部分是出队那里,如果把那里搞懂了,说明你的链表和队列都已经学扎实了。
希望大家能好好理解,祝各位学习愉快!
好了,五集的队列讲解连续剧已经讲完了,希望看到朋友对你有帮助!