關於 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;
}

運行截圖:
在這裏插入圖片描述


總結:
優先隊列最難的部分是出隊那裏,如果把那裏搞懂了,說明你的鏈表和隊列都已經學紮實了。

希望大家能好好理解,祝各位學習愉快!


好了,五集的隊列講解連續劇已經講完了,希望看到朋友對你有幫助!

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