這是算法與數據結構番外系列的第一篇,這個系列將來的主要內容是補充一些與算法與數據結構相關的知識,這些知識比較零碎,同時也與正傳關係密切,每每須要閱讀了正傳的相關內容之後,才能較好的理解這部份內容。若是對番外系列不感興趣的話,是能夠跳過此係列內容的,不會影響理解其餘文章的內容。git
此文優先隊列的代碼能夠在個人 github 上查看。算法
優先隊列是一種特殊的隊列。隊列具備先進先出的特性,對於普通隊列而言,首先出隊的元素是首先入隊的元素,而優先隊列中,首先出隊的元素是目前隊列中優先級最高的元素。數組
優先隊列分爲兩類,最大優先隊列和最小優先隊列。一個最大優先隊列的嚴格定義以下:數據結構
最大優先隊列是一種用來維護由一組元素構成的集合 S 的數據結構,其中的每個元素都有一個相關的值,稱爲關鍵字(key,也就是咱們上文所說的優先級),一個最大優先隊列應該支持如下操做:函數
INSERT(S, x):把元素 x 插入集合 S 中。spa
MAXIMUM(S):返回 S 中具備最大關鍵字的元素。code
EXTRACT-MAX(S):去掉並返回 S 中的具備最大關鍵字的元素。blog
最大優先隊列最爲著名的應用場景,是共享計算機系統的做業調度。最大優先隊列將會記錄將要執行的各個做業以及它們之間的相對優先級。當一個做業完成或者被中斷後,調度器調用 EXTRACT-MAX 從全部的等待做業中,選出具備最高優先級的做業來執行,在任什麼時候候,調度器均可以調用 INSERT 把一個新做業加入到隊列中來。排序
相應的,最小優先隊列則是選擇集合中具備最小關鍵字的元素。最小優先隊列常常被用於模擬。隊列中保存要模擬的事件,每一個事件都有一個發生時間做爲其關鍵字,事件將會按照時間前後順序依次發生。模擬程序調用 EXTRACT-MIN (即與 EXTRACT-MAX 正好相反的功能:去掉並返回 S 中的具備最小關鍵字的元素)選擇下一個要模擬的事件,當一個新事件產生時,模擬器經過調用 INSERT 將其插入最小優先隊列。
優先隊列可使用堆來實現,最大優先隊列對應最大堆,最小優先隊列對應最小堆。下面咱們將以最大優先隊列爲例進行介紹。
優先隊列的定義與隊列幾乎徹底同樣:
// 優先隊列定義 typedef struct PriorityQueue{ int* array; int max_size; }PriorityQueue;
這些輔助函數咱們在堆排序的那一章中已經寫好,這裏能夠直接使用。
獲取堆中節點的父節點,左孩子和右孩子節點下標:
#define PARENT(i) (i / 2) #define LEFT(i) (2 * i) #define RIGHT(i) (2 * i + 1)
交換數組兩個元素的值:
// 交換數組 下標i 和 下標j 對應的值 int swap(int *array, int i, int j){ int temp; temp = array[i]; array[i] = array[j]; array[j] = temp; return 0; }
遞歸維護最大堆:
// 遞歸維護最大堆 int MaintainMaxHeap(int *heap, int i){ int largest; int left = LEFT(i); int right = RIGHT(i); if(left <= heap[0] && heap[left] > heap[i]){ largest = left; } else{ largest = i; } if(right <= heap[0] && heap[right] > heap[largest]){ largest = right; } if(largest != i){ swap(heap, largest, i); MaintainMaxHeap(heap, largest); } return 0; }
這些輔助函數是直接採用的堆排序所用代碼,因爲篇幅有限,故再也不重複解釋,能夠點此查看相關解釋。
// 初始化優先隊列 PriorityQueue* PriorityQueueInit(int max_size){ PriorityQueue* priority_queue = (PriorityQueue*)malloc(sizeof(PriorityQueue)); priority_queue->array = (int*)malloc(sizeof(int) * (max_size + 1)); priority_queue->array[0] = 0; priority_queue->max_size = max_size; return priority_queue; }
咱們在這裏,依然使用堆排序中的數組解釋方法:array[0] 用於儲存堆中的有效數據個數,故數組的實際長度爲 max_size + 1,堆頂元素是 array[1]。
// 優先隊列入隊 int PriorityQueueEnqueue(PriorityQueue *priority_queue, int number_to_enqueue){ int i; priority_queue->array[0] += 1; i = priority_queue->array[0]; priority_queue->array[priority_queue->array[0]] = number_to_enqueue; while(i > 1 && priority_queue->array[PARENT(i)] < priority_queue->array[i]){ swap(priority_queue->array, PARENT(i), i); } return 0; }
整個最大優先隊列本質上是一個最大堆,當咱們插入一個數據時,首先將其插入至堆的尾部,此時可能會違背最大堆的性質,故咱們將此元素不斷與其父節點的值進行比較,若其小於父節點的值,說明此時整個堆已是一個最大堆了;若其大於父節點的值,則將此節點與父節點交換,重複此步驟,直到此元素小於其父節點的值或此元素成爲了堆頂節點。
顯然,入隊操做的時間複雜度是 \(O(lgn)\) ,由於整個函數中影響其時間複雜度的過程爲 while 循環,其最差狀況是將此元素從葉節點一步一步交換至根節點,而樹的高度爲 \(O(lgn)\) 。
整個過程以下圖所示:
// 優先隊列隊首元素 int PriorityQueueHead(PriorityQueue *priority_queue){ return priority_queue->array[1]; } // 優先隊列出隊 int PriorityQueueDequeue(PriorityQueue *priority_queue){ int return_number = priority_queue->array[1]; priority_queue->array[1] = priority_queue->array[priority_queue->array[0]]; priority_queue->array[0] -= 1; MaintainMaxHeap(priority_queue->array, 1); return return_number; }
在理解了堆排序之後,優先隊列的出隊操做很簡單了。
一個最大優先隊列出隊時返回的值爲隊列中的最大值,即 array[1],那麼咱們只須要像堆排序時那樣,將堆中最後一個有效數據複製到堆頂(array[1]),此時新的堆頂可能會違反最大堆的性質,爲此咱們只需對堆頂元素調用一次 MaintainMaxHeap() ,便可保證出隊後,此堆依然是一個最大堆。
關於此過程的時間複雜度和正確性分析,咱們已經在堆排序一章中介紹過了,在此就不贅述了,直接給出結果 \(O(lgn)\) 。
在理解了堆排序之後,優先隊列就很是簡單了,幾乎連代碼都不須要太多修改。從上文的分析,咱們也能得出一個結論,優先隊列最爲關鍵的兩個步驟(入隊、出隊)的時間複雜度均爲 \(O(lgn)\) 。
原文連接:albertcode.info
我的博客:albertcode.info