JS實現堆排序

堆的預備知識

  • 堆是一個徹底二叉樹。
  • 徹底二叉樹: 二叉樹除開最後一層,其餘層結點數都達到最大,最後一層的全部結點都集中在左邊(左邊結點排列滿的狀況下,右邊才能缺失結點)。
  • 大頂堆:根結點爲最大值,每一個結點的值大於或等於其孩子結點的值。
  • 小頂堆:根結點爲最小值,每一個結點的值小於或等於其孩子結點的值。
  • 堆的存儲: 堆由數組來實現,至關於對二叉樹作層序遍歷。以下圖:

clipboard.png

clipboard.png

對於結點 i ,其子結點爲 2i+1 與 2i+2 。算法

堆排序算法

clipboard.png

如今須要對如上二叉樹作升序排序,總共分爲三步:api

  1. 將初始二叉樹轉化爲大頂堆(heapify)(實質是從第一個非葉子結點開始,從下至上,從右至左,對每個非葉子結點作shiftDown操做),此時根結點爲最大值,將其與最後一個結點交換。
  2. 除開最後一個結點,將其他節點組成的新堆轉化爲大頂堆(實質上是對根節點作shiftDown操做),此時根結點爲次最大值,將其與最後一個結點交換。
  3. 重複步驟2,直到堆中元素個數爲1(或其對應數組的長度爲1),排序完成。

下面詳細圖解這個過程:數組

步驟1:

初始化大頂堆,首先選取最後一個非葉子結點(咱們只須要調整父節點和孩子節點之間的大小關係,葉子結點之間的大小關係無需調整)。設數組爲arr,則第一個非葉子結點的下標爲:i = Math.floor(arr.length/2 - 1) = 1,也就是數字4,如圖中虛線框,找到三個數字的最大值,與父節點交換。函數

clipboard.png

而後,下標 i 依次減1(即從第一個非葉子結點開始,從右至左,從下至上遍歷全部非葉子節點)。後面的每一次調整都是如此:找到父子結點中的最大值,作交換。spa

clipboard.png

這一步中數字六、1交換後,數字[1,5,4]組成的堆順序不對,須要執行一步調整。所以須要注意,每一次對一個非葉子結點作調整後,都要觀察是否會影響子堆順序!操作系統

clipboard.png

此次調整後,根節點爲最大值,造成了一個大頂堆,將根節點與最後一個結點交換。3d

步驟2:

除開當前最後一個結點6(即最大值),將其他結點[4,5,3,1]組成新堆轉化爲大頂堆(注意觀察,此時根節點之外的其餘結點,都知足大頂堆的特徵,因此能夠從根節點4開始調整,即找到4應該處於的位置便可)。code

clipboard.png

clipboard.png

步驟3:

接下來反覆執行步驟2,直到堆中元素個數爲1:blog

clipboard.png

clipboard.png

clipboard.png

堆中元素個數爲1, 排序完成。排序

JavaScript實現

// 交換兩個節點
function swap(A, i, j) {
  let temp = A[i];
  A[i] = A[j];
  A[j] = temp; 
}

// 將 i 結點如下的堆整理爲大頂堆,注意這一步實現的基礎其實是:
// 假設 結點 i 如下的子堆已是一個大頂堆,shiftDown函數實現的
// 功能是其實是:找到 結點 i 在包括結點 i 的堆中的正確位置。後面
// 將寫一個 for 循環,從第一個非葉子結點開始,對每個非葉子結點
// 都執行 shiftDown操做,因此就知足告終點 i 如下的子堆已是一大
//頂堆
function shiftDown(A, i, length) {
  let temp = A[i]; // 當前父節點
// j<length 的目的是對結點 i 如下的結點所有作順序調整
  for(let j = 2*i+1; j<length; j = 2*j+1) {
    temp = A[i];  // 將 A[i] 取出,整個過程至關於找到 A[i] 應處於的位置
    if(j+1 < length && A[j] < A[j+1]) { 
      j++;   // 找到兩個孩子中較大的一個,再與父節點比較
    }
    if(temp < A[j]) {
      swap(A, i, j) // 若是父節點小於子節點:交換;不然跳出
      i = j;  // 交換後,temp 的下標變爲 j
    } else {
      break;
    }
  }
}

// 堆排序
function heapSort(A) {
  // 初始化大頂堆,從第一個非葉子結點開始
  for(let i = Math.floor(A.length/2-1); i>=0; i--) {
    shiftDown(A, i, A.length);
  }
  // 排序,每一次for循環找出一個當前最大值,數組長度減一
  for(let i = Math.floor(A.length-1); i>0; i--) {
    swap(A, 0, i); // 根節點與最後一個節點交換
    shiftDown(A, 0, i); // 從根節點開始調整,而且最後一個結點已經爲當
                         // 前最大值,不須要再參與比較,因此第三個參數
                         // 爲 i,即比較到最後一個結點前一個便可
  }
}

let Arr = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
heapSort(Arr);
alert(Arr);

程序註釋: 將 i 結點如下的堆整理爲大頂堆,注意這一步實現的基礎其實是:假設 結點 i 如下的子堆已是一個大頂堆,shiftDown函數實現的功能是其實是:找到 結點 i 在包括結點 i 的堆中的正確位置。後面作第一次堆化時,heapSort 中寫了一個 for 循環,從第一個非葉子結點開始,對每個非葉子結點都執行 shiftDown操做,因此就知足了每一次 shiftDown中,結點 i 如下的子堆已是一大頂堆。

複雜度分析:adjustHeap 函數中至關於堆的每一層只遍歷一個結點,由於
具備n個結點的徹底二叉樹的深度爲[log2n]+1,因此 shiftDown的複雜度爲 O(logn),而外層循環共有 f(n) 次,因此最終的複雜度爲 O(nlogn)。

堆的應用

堆主要是用來實現優先隊列,下面是優先隊列的應用示例:

  • 操做系統動態選擇優先級最高的任務執行。
  • 靜態問題中,在N個元素中選出前M名,使用排序的複雜度:O(NlogN),使用優先隊列的複雜度: O(NlogM)。

而實現優先隊列採用普通數組、順序數組和堆的不一樣複雜度以下:

clipboard.png

使用堆來實現優先隊列,可使入隊和出隊的複雜度都很低。

相關文章
相關標籤/搜索