1. 程式人生 > >排序演算法大雜燴之堆排序

排序演算法大雜燴之堆排序

排序演算法大雜燴主幹文章傳送門

堆排序

#include <iostream>
#include <vector>
/*
非降序排序
時間複雜度:o(nlgn)
空間複雜度:o(nlgn)
建堆:o(n)
維護堆:o(lgn)
不穩定
*/
using namespace std;

void swap(int &a,int &b){int c = a;a = b;b = c;}

int Max(const vector<int> &a,int n,int i){
    if(2*i + 2 > n) return
i;//該節點左孩子不存在 if(2*i + 3 > n) { //該節點右孩子不存在 if(a[i] >= a[2*i + 1]) return i; else return 2*i + 1; } if(a[i] >= a[2*i + 1] && a[i] >= a[2*i + 2]) return i; if(a[2*i + 1] >= a[i] && a[2*i + 1] >= a[2*i + 2]) return 2*i + 1; return
2*i + 2; } void heapify(vector<int>&a,int n,int i)//lgn,即堆高度 { //最大堆 //n 為堆大小 //i 是指下標 int cur = i; while(true){ int tmp = Max(a,n,cur); if(tmp == cur) break; swap(a[cur],a[tmp]); cur = tmp; } } //建堆過程 void buildHeap(vector<int> &a)
//n { for(int i = a.size()/2 - 1;i >= 0;i --){ heapify(a,a.size(),i); } } //堆排序過程 void heapSort(vector<int> &a) { buildHeap(a); for(auto c : a) cout << c << " "; cout << endl; for(int i = a.size() - 1;i > 0;i --){ swap(a[i],a[0]); heapify(a,i,0); for(auto c : a) cout << c << " "; cout << endl; } } int main() { vector<int> a = {4,1,3,2,16,9,10,14,8,7}; heapSort(a); for(auto c : a) cout << c << " "; return 0; }
  • 基本概念
    • 最大堆:對於任何一個結點的鍵值總大於孩子結點的鍵值
    • 最小堆:對於任何一個結點的鍵值總小於孩子節點的鍵值
  • 思維引擎
    堆排序顧名思義,利用這種資料結構達到排序的目的。這裡主要運用了三個主要操作:建堆,維護堆,堆排序
    • 問題描述:對序列 A [ 1.. n ] A[1..n] 進行非降序排序。
    • 建堆:將原序列生成最大(小)堆的過程。
      n 2 節點 \frac{n}{2} 1 節點 1 依次維護以該節點為根節點的子樹的最大(小)堆性質。
    • 維護堆
      由於僅有根節點破壞了堆的性質,調整方案如下:
      1. 將子樹的根節點作為起始節點, C u r 節點Cur
      2. (以最大堆為例)若 C u r 節點Cur 小於孩子結點,則與孩子結點交換,並將 C u r 節點Cur 用此孩子更新。
      3. 重複操作 2 2 直至 C u r 節點Cur 大於孩子結點,那麼此時形成的堆就為最大堆。
    • 堆排序
      1. 將原始序列 A [ 1.. n ] A[1..n] 建成最大堆。
      2. 將 A [ 1 ] A[1] A [ n ] A[n] 中的元素交換,那麼 A [ n ] A[n] 儲存了序列的最大值,而 A [ 1.. n 1 ] A[1 .. n-1] 則成了僅 A [ 1 ] A[1] 不滿足最大堆性質的堆,對堆 A [ 1.. n 1 ] A[1 .. n-1] 進行堆維護。
      3. 在依次將 A [ 1 ] A[1] A [ n 1 ] A[n-1] A [ 2 ] A[2] 交換,重複操作 2 2 ,使得 A [ 1.. n ] A[1..n] 全部排序。
    • 值得注意的是在對維護的過程中,發現以 i 節點i 為根節點的子堆中僅有 i 節點i 不滿足最大(小)堆性質
  • 時間複雜度
    堆排序的時間複雜度是一個十分有趣的問題的,裡面的一些數學背景我會在後面一一指出,並給出相關的證明。整體的思路來源於演算法導論第三版相應內容。
    1. 維護堆:這裡很容易可以觀察到維護堆的時間複雜度與子樹的高度有關(子樹根節點與孩子結點置換時最多隻能置換子樹高度次,所以推測時間複雜度 o ( l o g n ) o(logn) ,下面是數學證明:
      T ( n ) T ( 2 3 n ) + o ( 1 ) , 滿 T ( n ) = o ( l o g n ) T(n) \leq T( \frac{2}{3} n) + o(1), \\當且僅當為三層最大堆且最後一層半滿時取等 \\ \Rightarrow T(n) = o(logn)
      這邊我們要指出為什麼偏偏是 2 3 n \frac{2}{3}n 不是其他的呢,這裡我們首先假設最後一層半滿,那麼我就可以得出最後一層(記為第 h h 層)的節點個數為 1 3 n \frac{1}{3}n ,那麼根節點的左子樹的的節點總個數就是 2 3 n \frac{2}{3}n ,並且在維護堆的過程中我們只能堆根節點的左子樹( 2 3 n \frac{2}{3}n 個節點)或者右子樹( 1 3 n \frac{1}{3}n 個節點)進行操作,顯然上述不等式成立。
    2. 建堆
      這個時間複雜度是很坑的,當直接從程式碼本身去觀察,對於 A [ 1.. i . . 1 2 n ] A[1.. i ..\frac{1}{2}n] 個節點,每一個節點都需要 o ( l o g n ) o(logn) 左右的複雜度,所以是 o ( n l o g n ) o(nlogn) 然而啪啪打臉。
      H = l o g n T ( n ) = Σ h = 1 H l o g ( 2 H h + 1 1 ) 2 h 1 Σ h = 1 H l o g ( 2 H h + 1 ) 2 h 1 , l o