當我們要在一組數據中找到最小/大值或者前K大/小值的時候,我們可以使用傳統的遍歷方法。那么這個時候時間復雜度就是$O(N^2)$,但我們可以使用"堆"來進行優化,我們可以把找到最小/大值的復雜度降低到$O(logN)$。插入一個新值的復雜度也是$O(logN)$。
維護一個堆關鍵的就是向下維護和向上維護,基於這兩種方法我們就可以實現插入,刪除
向下調整,時間復雜度:$O(logn)$
建堆,時間復雜度:$O(n)$
1 //建立大根堆 2 3 const int maxn = 100; 4 //heap為堆,n為元素個數 5 int heap[maxn],n=10; 6 7 void downAdjust(int low,int high) 8 { 9 int i = low, j = i*2; 10 while(j <= high) 11 { 12 if(j+1<=high && heap[j+1]>heap[j]) 13 ++j; 14 if(heap[j] > heap[i]) 15 { 16 swap(heap[j],heap[i]); 17 i = j; 18 j = i*2; 19 } 20 else 21 break; 22 } 23 } 24 25 void createHeap() 26 { 27 for(int i=n/2;i>=1;--i) 28 { 29 downAdjust(i,n); 30 } 31 }
完全二叉樹的葉子結點個數為$\biggl\lceil\frac{n}{2}\biggr\rceil\qquad$,因此數組下標在$[1,\biggl\lfloor\frac{n}{2}\biggr\rfloor]$范圍內的結點都是非葉子結點。於是可以從$\biggl\lfloor\frac{n}{2}\biggr\rfloor]$號位開始倒着枚舉結點。倒着枚舉保證了每個結點都是以其為根節點的子樹中權值最大的結點。
刪除堆頂元素 時間復雜度:$O(logn)$
1 void deleteTop() // 刪除堆頂元素 時間復雜度:O(logn) 2 { 3 heap[1] = heap[n--]; 4 downAdjust(1,n); 5 } 6 向上調整,時間復雜度:O(logn) 7 void upAdjust(int low,int high) 8 { 9 int i = high,j = i/2; 10 while(j>=low) 11 { 12 if(heap[j] < heap[i]) 13 { 14 swap(heap[j],heap[i]); 15 i = j; 16 j = i/2; 17 } 18 else 19 break; 20 } 21 } 22 23 void insert(int x) // 添加元素 24 { 25 heap[++n] = x; 26 upAdjust(1,n); 27 }
堆排序
具體實現時,為了節省空間,可以倒着遍歷數組,假設當前訪問到i號位,那么將堆頂元素與i號位的元素交換,接着在[1,i-1]范圍內對堆頂元素進行一次向下調整
1 void heapSort() 2 { 3 createHeap(); // 建堆 4 for(int i=n;i>1;--i) 5 { 6 swap(heap[i],heap[1]); 7 downAdjust(1,i-1); 8 } 9 }
以上的就是最大堆的實現,最小堆只要把判斷大小換過來便是了
Dijkstra算法也可以使用堆來優化找離源點最近的頂點的過程,使時間復雜度下降到$O(M+N)logN$,堆還可以用來求一個數列中第K大的數:首先建立一個大小為K的最小堆,從第K+1個數開始,與堆頂進行比較,如果比堆頂大則代替堆頂並維護,比堆頂小則直接舍棄。這樣最后堆頂便是第K大數。時間復雜度為$O(NlogK)$。