堆排序基本原理及實現


一、堆的概念

我們一般提到堆排序里的堆指的是二叉堆(binary heap),是一種完全二叉樹,二叉堆有兩種:最大堆和最小堆,特點是父節點的值大於(小於)兩個小節點的值。

 

二、基礎知識

完全二叉樹有一個性質是,除了最底層,每一層都是滿的,這使得堆可以利用數組來表示,每個結點對應數組中的一個元素,如下圖所示

heap-and-array

對於給定的某個結點的下標 i(從1開始),可以很容易的計算出這個結點的父結點、孩子結點的下標:

  • Parent(i) = floor(i/2),i 的父節點下標
  • Left(i) = 2i,i 的左子節點下標
  • Right(i) = 2i + 1,i 的右子節點下標

但是數組都是0基的,所以調整下標之后,對應關系如下圖所示:

heap-and-array-zero-based

因此前面說到的關系也要隨之調整:

  • 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的位置,使得子節點永遠小於父節點,過程如下圖所示:

MAX‐HEAPIFY-Procedure

由於一次調整后,堆仍然違反堆性質,所以需要遞歸的測試,使得整個堆都滿足堆性質,用 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。流程如下:

building-a-heap

用 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次之后,數組排列完畢。如果是從小到大排序,用大頂堆;從大到小排序,用小頂堆。流程如下:

HeapSort

用 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. 堆排序

2. 常見排序算法 - 堆排序 (Heap Sort)

3. 堆排序(Heap Sort)算法學習

4. 堆排序的時間復雜度

(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM