優先級隊列,也叫二叉堆、堆(不要和內存中的堆區搞混了,不是一個東西,一個是內存區域,一個是數據結構)。算法
堆的本質上是一種徹底二叉樹,分爲:api
最小堆(小根堆):樹中每一個非葉子結點都不大於其左右孩子結點的值,也就是根節點最小的堆,圖(a)。數組
最大堆(大根堆):樹中每一個非葉子結點都不小於其左右孩子結點的值,也就是根節點最大的堆,圖(b)。數據結構
均以大根堆爲例spa
堆本質上是一顆徹底二叉樹,使用數組進行存儲,從\(a[1]\)開始存儲,這樣對於下標爲\(k\)的結點\(a[k]\)來講,其左孩子的下標爲\(2*k\),右孩子的下標爲\(2*k+1\)。,且不論 \(k\) 是奇數仍是偶數,其父親結點(若是有的話)就是 $\left \lfloor k/2 \right \rfloor $。code
假如咱們向一個堆中插入一個元素,要使其仍然保持堆的結構。應該怎麼辦呢?blog
能夠把想要添加的元素放在數組的最後,也就是徹底二叉樹的最後一個結點的後面,而後進行向上調整(heapinsert)。向上調整老是把欲調整結點與父親結點比較,若是權值比父親結點大,那麼就交換其與父親結點,反覆比較,直到到達堆頂或父親結點的值較大爲止。向上調整示意圖以下:排序
代碼以下,時間複雜度爲\(O(logn)\):隊列
void heapinsert(int* arr, int n) { int k = n; //若是 K 結點有父節點,且比父節點的權值大 while (k > 1 && arr[k] > arr[k / 2]) { //交換 K 與父節點的值 swap(arr[k / 2], arr[k]); k >>= 1; } }
這樣添加元素就很簡單了內存
void insert(int* arr, int n, int x) { arr[++n] = x;//將x置於數組末尾 heapinsert(arr, n);//向上調整x }
假如咱們要刪除一個堆中的堆頂元素,要使其仍然保持堆的結構。應該怎麼辦呢?
移除堆頂元素後,將最後一個元素移動到堆頂,而後對這個元素進行向下調整(heapify),向下調整老是把欲調整結點 \(K\) 與其左右孩子結點比較,若是孩子中存在權值比當前結點 \(K\) 大的,那麼就將其中權值最大的那個孩子結點與結點 \(K\),反覆比較,直到到結點 \(K\) 爲葉子結點或結點 \(K\) 的值比孩子結點都大爲止。向下調整示意圖以下:
代碼以下,時間複雜度也是\(O(logn)\):
void heapify(int* arr, int k, int n) { //若是結點 K 存在左孩子 while (k * 2 <= n) { int left = k * 2; //若是存在右孩子,而且右孩子的權值大於左孩子 if (left + 1 <= n && arr[left] < arr[left + 1]) left++; //就選中右孩子 //若是節點 K 的權值已經大於左右孩子中較大的節點 if (arr[k] > arr[left]) break; swap(arr[left], arr[k]); k = left; } }
這樣刪除堆頂元素也就變得很簡單了
void deleteTop(int* arr, int n) { arr[1] = arr[n--];//用最後一個元素覆蓋第一個元素,並讓n-1 heapify(arr, 1, n); }
自頂向下建堆的思想是,從第 \(i=1\) 個元素開始,對其進行向上調整,始終使前 \(i\) 個元素保持堆的結構。時間複雜度 \(O(nlogn)\)
void ArrayToHeap(int *a,int n) { for (int i = 1; i <= n; i++) { heapinsert(a, i); } }
自底向上建堆的思想是,從底 $i=\left \lfloor n/2 \right \rfloor $ 個元素開始,對其進行向下調整,始終讓後 \(n-i\) 個元素保持堆的結構。
void ArrayToBheap(int *a, int n) { int i = n / 2; for (; i >= 1; i--) { heapify(a, i, n); } }
若是僅從代碼上直觀觀察,會得出構造二叉堆的時間複雜度爲\(O(nlogn)\)的結果。固然這個上界是正確的,但卻不是漸近界,能夠觀察到,不一樣結點在運行 heapify
的時間與該結點的樹高(樹高是指該結點到最底層葉子結點的值,不要和深度搞混了)相關,並且大部分結點的高度都很小。利用如下性質能夠獲得一個更準確的漸近界:
【能夠畫顆樹試一下,具體證實請看算法導論】
在一個高度爲 \(h\) 的結點上運行 heapify
的代價爲 \(O(h)\),咱們能夠將自頂向下建堆的總複雜度表示爲
\[ \sum ^{h}_{k=0} \lceil \frac {n}{2^{k+1}} \rceil O(h)= O(n\sum ^{h} _{k=0}\frac {k}{2^{k}}) \]
這個式子
\[ \sum ^{h} _{k=0}\frac {k}{2^{k}} \]
其實就是求前 \(n\) 項和,高中數學的知識
\[ T(k)=\frac{1}{2}+\frac{2}{2^2}+\frac{3}{2^3}+\cdots+\frac{k}{2^k}\\\frac{1}{2}T(k)=\frac{1}{2^2}+\frac{2}{2^3}+\frac{3}{2^4}+\cdots+\frac{k-1}{2^k}+\frac{k}{2^k+1}\\T(k)-\frac{1}{2}T(k)=\frac{1}{2}+\frac{1}{2^2}+\frac{1}{2^3}+\cdots+\frac{1}{2^k}-\frac{k}{2^{k+1}}\\\frac{1}{2}T(k)=\frac{\frac{1}{2}(1-(\frac{1}{2})^k)}{1-\frac{1}{2}}-\frac{k}{2^{k+1}}\\T(k)=2-\frac{1}{2^{k-1}}-\frac{k}{2^{k}} \]
到這兒就須要求極限,高等數學的知識 \(\frac{1}{2^{k-1}}\) 當 \(k\) 趨於無窮大時極限是 \(0\),對 \(\frac{k}{2^{k}}\) 用洛必達法則極限也是 \(0\)
也就是說當 \(h\) 趨向於無窮大時,\(O(n\sum ^{h} _{k=0}\frac {k}{2^{k}})=O(n\cdot 2)\) ,去掉常數項,因此自底向上建堆複雜度爲 \(O(n)\)
堆排序的思想:假設一個大根堆有 \(n\) 個元素,每次把第 \(1\) 個元素,與第 \(n\) 個元素交換,對第一個元素進行向下調整(heapify),並使得 \(n=n-1\) ,直到 \(n=1\)
void heapSort(int* arr, int n) { //先自底向上建堆 int i = n / 2; for (; i >= 1; i--) { heapify(arr, i, n); } for (int i = 50; i > 1; i--) { swap(arr[1], arr[i]); heapify(arr, 1, i - 1); } }
首先用數組的前k個元素構建一個小根堆,而後遍歷剩餘數組和堆頂比較,若是當前元素大於堆頂,則把當前元素放在堆頂位置,並調整堆(heapify)。遍歷結束後,堆頂就是數組的最大k個元素中的最小值,也就是第k大元素。
void heapify(int* a, int index, int length) { int left = index * 2 + 1; while (left <= length) { if (left + 1 <= length - 1 && a[left + 1] > a[left])left++; if (a[index] > a[left])break; swap(a[index], a[left]); index = left; } } void ArrayToBheap(int* a, int length) { int i = length / 2 - 1; for (; i >= 0; i--) { heapify(a, i, length); } } void FindKMax(int* a, int k, int length) { ArrayToBheap(a, k); for (int i = k; i < length; i++) { if (a[i] > a[0]) a[0] = a[i]; heapify(a, 0, k); } }
時間複雜度\(O(n)\),只是舉個例子。
事實上對於這個問題是有更快的作法的,快速排序的思想,時間複雜度 \(O(logn)\)
int Search_K(int left, int right, int k) { int i = left, j = right; int p = rand() % (right - left + 1) + left; int sign = a[p]; swap(a[p], a[i]); while (i < j) { while (i < j && a[j] >= sign)j--; while (i < j && a[i] <= sign)i++; swap(a[i], a[j]); } swap(a[i], a[left]); if (i - left + 1 == k)return a[i]; if (i - left + 1 < k)return Search_K(i + 1, right, k - (i - left + 1)); else return Search_K(left, i - 1, k); }
堆更多時候,由於它建堆\(O(n)\),調整\(O(logn)\),當須要有序獲得某些數據,是要優於排序(\(O(nlogn)\))算法的,並且若是數據規模是動態增長的,那堆就要徹底優於排序算法了,在C++的STL是有堆的實現的,叫作 priority_queue
。