关于 C++ 队列算法,你该了解这些【第五集:优先链式存储队列】

上集回顾:循环队列

第一集:顺序存储队列
第二集:链式存储队列
第三集:线性池中的任务队列
第四集:循环队列


观看本系列博文提醒:
你将学会队列的两种最基本的表现形式:顺序存储队列链式存储队列
一个扩展队列的使用方法:循环队列
两个企业级队列的应用:线性池中的任务队列优先链式存储队列


队列的原理

队列是一种受限的线性表,(Queue),它是一种运算受限的线性表,先进先出(FIFO First In First Out).
在这里插入图片描述
例如上图中,圆球1先进,也是圆球1先出。

队列是一种受限的线性结构

  1. 它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。
  2. 生活中队列场景随处可见: 比如在电影院, 商场, 或者厕所排队。。。。。。
    在这里插入图片描述
    由上图我们可以知道,队列中有两个“指针”,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;
}

入队他有两种情况:

  1. 队列为空
    在这里插入图片描述
    因为我们传入的是待插入的元素,所以我们先创建一个新的节点。
    队列插入元素后,队首指针和队尾指针都要指向第一个元素(如上图)。
    然后length自增一。

  2. 队列不为空
    在这里插入图片描述
    因为我们传入的是待插入的元素,所以我们先创建一个新的节点.
    头指针不需要动。尾指针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;
}

在这里插入图片描述
删除一共有三种情况:

  1. 删除的是中间的某个优先级最高的节点(如上图);
  2. 删除的是队列只剩下的唯一一个节点(如下图一);
  3. 删除的是队列最后一个节点(如下图二)。

在这里插入图片描述
在这里插入图片描述

不管有多少种情况,我们都可以用一种代码搞定!

  1. 找到优先级最高的那个节点;
    代码中,我们定义了四个临时变量:
	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。

如下图:
在这里插入图片描述

  1. 进行出队操作:
	tem = (*rear);			// 待出队节点赋值给tem,好释放内存
	*rear = (*rear)->next;	// 把待出队节点的前一个节点的next指向待出队节点的下一个节点
	delete tem;

	LQ->lenght -= 1;	// 最后将length减一

tem需要指向优先级最高的节点(用于释放);
*rear = (*rear)->next;是什么意思呢?
解析:
因为他的前一个节点是指向他的,所以*rear将自己的下一个节点赋值给自己,也就间接的把以自己为中心,将自己前后两个节点链起来,自己与他们断开。(如下图)
在这里插入图片描述
这里需要好好理解,小编当时在这里也卡了很久!这也是这套算法的绝妙之处,只有大牛才会这么写,因为这样的效率是很高的,这些代码也是一位大牛所教,所写的!

最后再将tem释放掉,也就是将优先级最高的节点的内存释放掉!
队列的长度也要减一。

  1. 接下来就可以判断剩下的两种情况了:

    1. 删除的是首节点,而且队列长度为零
    if (LQ->lenght == 0) {
    	LQ->rear = NULL;
    }
    

    如果队列的长度为零的话,说明删除的是队列中唯一的节点,即队列中已 经没有任何节点了。所以我们我要将队尾指针rear指向NULL。

    1. 删除的是尾部节点
    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循环遍历队列;

  1. 临时节点首先指向自己的下一个节点;
  2. 释放第一个节点;
  3. 再将临时节点赋值给释放掉的空节点;
  4. while循环结束后,还得将队首指针和队尾指针都指向NULL;
  5. 最后再将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;
}

运行截图:
在这里插入图片描述


总结:
优先队列最难的部分是出队那里,如果把那里搞懂了,说明你的链表和队列都已经学扎实了。

希望大家能好好理解,祝各位学习愉快!


好了,五集的队列讲解连续剧已经讲完了,希望看到朋友对你有帮助!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章