一、堆的概念
我們一般提到堆排序里的堆指的是二叉堆(binary heap),是一種完全二叉樹,二叉堆有兩種:最大堆和最小堆,特點是父節點的值大於(小於)兩個小節點的值。
二、基礎知識
完全二叉樹有一個性質是,除了最底層,每一層都是滿的,這使得堆可以利用數組來表示,每個結點對應數組中的一個元素,如下圖所示
對於給定的某個結點的下標 i(從1開始),可以很容易的計算出這個結點的父結點、孩子結點的下標:
- Parent(i) = floor(i/2),i 的父節點下標
- Left(i) = 2i,i 的左子節點下標
- Right(i) = 2i + 1,i 的右子節點下標
但是數組都是0基的,所以調整下標之后,對應關系如下圖所示:
因此前面說到的關系也要隨之調整:
- Parent(i) = floor((i-1)/2),i 的父節點下標
- Left(i) = 2i + 1,i 的左子節點下標
- Right(i) = 2(i + 1),i 的右子節點下標
三、堆的基本操作
1. 最大堆調整(Max-Heapify)
該操作主要用於維持堆的基本性質。假設數組A和下標i,假定以Left(i)和Right(i)為根結點的左右兩棵子樹都已經是最大堆,節點i的值可能小於其子節點。調整節點i的位置,使得子節點永遠小於父節點,過程如下圖所示:
由於一次調整后,堆仍然違反堆性質,所以需要遞歸的測試,使得整個堆都滿足堆性質,用 JavaScript 可以表示如下:
/** * 從 index 開始檢查並保持最大堆性質 * * @array * * @index 檢查的起始下標 * * @heapSize 堆大小 * **/ function maxHeapify(array, index, heapSize) { var iMax = index, iLeft = 2 * index + 1, iRight = 2 * (index + 1); if (iLeft < heapSize && array[index] < array[iLeft]) { iMax = iLeft; } if (iRight < heapSize && array[iMax] < array[iRight]) { iMax = iRight; } if (iMax != index) { swap(array, iMax, index); maxHeapify(array, iMax, heapSize); // 遞歸調整 } } function swap(array, i, j) { var temp = array[i]; array[i] = array[j]; array[j] = temp; }
2. 創建最大堆(Build-Max-Heap)
創建最大堆(Build-Max-Heap)的作用是將一個數組改造成一個最大堆,接受數組和堆大小兩個參數,Build-Max-Heap 將自下而上的調用 Max-Heapify 來改造數組,建立最大堆。因為 Max-Heapify 能夠保證下標 i 的結點之后結點都滿足最大堆的性質,所以自下而上的調用 Max-Heapify 能夠在改造過程中保持這一性質。如果最大堆的數量元素是 n,那么 Build-Max-Heap 從 Parent(n) 開始,往上依次調用 Max-Heapify。流程如下:
用 JavaScript實現:
function buildMaxHeap(array, heapSize) { var i, iParent = Math.floor((heapSize - 1) / 2); for (i = iParent; i >= 0; i--) { maxHeapify(array, i, heapSize); } }
3. 堆排序(Heap-Sort)
堆排序(Heap-Sort)是堆排序的接口算法,Heap-Sort先調用Build-Max-Heap將數組改造為最大堆,然后將堆頂和堆底元素交換,之后將底部上升,最后重新調用Max-Heapify保持最大堆性質。由於堆頂元素必然是堆中最大的元素,所以一次操作之后,堆中存在的最大元素被分離出堆,重復n-1次之后,數組排列完畢。如果是從小到大排序,用大頂堆;從大到小排序,用小頂堆。流程如下:
用 JavaScript實現:
function heapSort(array, heapSize) { buildMaxHeap(array, heapSize); for (int i = heapSize - 1; i > 0; i--) { swap(array, 0, i); maxHeapify(array, 0, i); } }
四、時間復雜度與排序穩定性
我們知道n個元素的完全二叉樹的深度h=floor(logn),分析各個環節的時間復雜度如下。
1. 堆調整時間復雜度
從堆調整的代碼可以看到是當前節點與其子節點比較兩次,交換一次。父節點與哪一個子節點進行交換,就對該子節點遞歸進行此操作,設對調整的時間復雜度為T(k)(k為該層節點到葉節點的距離),那么有:
- T(k)=T(k-1)+3, k∈[2,h]
- T(1)=3
迭代法計算結果為:
- T(h)=3h=3floor(log n)
所以堆調整的時間復雜度是O(log n) 。
2. 建堆的時間復雜度
n個節點的堆,樹高度是h=floor(log n)。
對深度為於h-1層的節點,比較2次,交換1次,這一層最多有2^(h-1)個節點,總共操作次數最多為3(12^(h-1));對深度為h-2層的節點,總共有2^(h-2)個,每個節點最多比較4次,交換2次,所以操作次數最多為3(22^(h-2))……
以此類推,從最后一個父節點到根結點進行堆調整的總共操作次數為:
s=3*[2^(h-1) + 2*2^(h-2) + 3*2^(h-3) + … + h*2^0] a 2s=3*[2^h + 2*2^(h-1) + 3*2(h-2) + … + h*2^1] b b-a,得到一個等比數列,根據等比數列求和公式 s = 2s - s = 3*[2^h + 2^(h-1) + 2^(h-2) + … + 2 - h]=3*[2^(h+1)- 2 - h]≈3*n
所以建堆的時間復雜度是O(n)。
3. 堆排序時間復雜度
從上面的代碼知道,堆排序的時間等於建堆和進行堆調整的時間之和,所以堆排序的時間復雜度是O(nlog n + n) =O(nlog n)。
4. 穩定性
堆排序是不穩定的算法,它不滿足穩定算法的定義。它在交換數據的時候,是比較父結點和子節點之間的數據,所以,即便是存在兩個數值相等的兄弟節點,它們的相對順序在排序也可能發生變化。
五、參考
1. 堆排序
4. 堆排序的時間復雜度
(完)