淺解前端必須掌握的演算法(五):堆排序(下)

程式猿何大叔發表於2019-03-03

前言

雖然前端面試中很少會考到演算法類的題目,但是你去比如像騰訊一樣的大廠面試的時候就知道了,對基本演算法的掌握對於從事電腦科學技術的我們來說,還是必不可少的,每天花上 10 分鐘,輕鬆瞭解基本演算法概念以及前端的實現方式。

另外,掌握了一些基本的演算法實現,對於我們日常開發來說,也是如虎添翼,能讓我們的 js 業務邏輯更趨高效和流暢。

特別說明

我給每篇文章的定位是 10 分鐘內應該要掌握下來,由於知識結構需要構建全面些,我就擅作主張地將堆排序演算法講解分割為上、下兩篇文章了,希望能讓大家閱讀起來更清爽愉快。

各位看官都應該手癢癢想寫演算法了,今天我們就一起來看如何用 js 來實現堆排序。

文章結構

《堆排序(上)》文章結構:

  • 簡單的二叉樹
  • 簡單的滿二叉樹
  • 簡單的完全二叉樹
  • 簡單的堆
  • 簡單的堆分類

《堆排序(下)》文章結構:

  • 演算法介紹
  • 輕鬆實現大頂堆調整
  • 輕鬆實現建立大頂堆
  • 輕鬆實現堆排序
  • 複雜度分析

演算法介紹

堆排序,就是利用堆(假設利用大頂堆)進行排序的方法。

它的基本思想是,將待排序的陣列構造成一個大頂堆。此時整個陣列的最大值就是堆頂的根節點。將它移走,其實就是將其與堆陣列的末尾元素交換,此時末尾元素就是最大值。然後將剩餘的 n-1 個元素又重新構造成堆,這樣就又能得到次大值。
如此反覆操作,直到只剩餘一個元素,就能得到一個有序陣列了。

根據以上的演算法指導,可理出如下關鍵操作:

  • 大頂堆調整(Max-Heapify),將堆的末端子節點做調整,使得子節點永遠小於父節點;
  • 建立大頂堆(Build-Max-Heap),將堆中所有資料調整位置,使其成為大頂堆;
  • 堆排序(Heap-Sort),移除在堆頂的根節點,並做大頂堆調整的迭代運算。

輕鬆實現大頂堆調整

大頂堆調整(Max-Heapify)的作用是保持大頂堆的性質,是建立大頂堆的核心子程式,一次作用過程如下圖所示:

一次大頂堆調整示意圖

一次大頂堆調整示意圖

由於一次調整後,仍有可能出現違反大頂堆的性質,所以需要遞迴地進行調整,直到整個堆都滿足了條件。

/**
 * 從 index 開始檢查並保持大頂堆性質
 * @arr 待排序陣列
 * @index 檢查的起始下標
 * @heapSize 堆大小
 **/
var maxHeapify = function(arr, index, heapSize) {
  // 計算某節點與其左右子節點在位置上的關係
  // 上一節講過
  var iMax = index,
      iLeft = 2 * index + 1,
      iRight = 2 * (index + 1);

  // 是否左子節點比當前節點的值更大
  if (iLeft < heapSize && arr[index] < arr[iLeft]) {
    iMax = iLeft;
  }
  // 是否右子節點比當前更大節點的值更大
  if (iRight < heapSize && arr[iMax] < arr[iRight]) {
    iMax = iRight;
  }

  // 如果三者中,當前節點值不是最大的
  if (iMax != index) {
    swap(arr, iMax, index);
    maxHeapify(arr, iMax, heapSize); // 遞迴調整
  }
};
var swap = function(arr, i, j) {
  var temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
};
複製程式碼

上面程式碼將有個隱患,那就是當陣列長度很大時,其中的遞迴函式有可能會引起記憶體爆棧。那我們不妨來用迭代來實現:

var maxHeapify = function(arr, index, heapSize) {
  var iMax, iLeft, iRight;
  do {
    iMax = index;
    iLeft = 2 * index + 1;
    iRight = 2 * (index + 1);

    // 是否左子節點比當前節點的值更大
    if (iLeft < heapSize && arr[index] < arr[iLeft]) {
      iMax = iLeft;
    }
    // 是否右子節點比當前更大節點的值更大
    if (iRight < heapSize && arr[iMax] < arr[iRight]) {
      iMax = iRight;
    }

    // 如果三者中,當前節點值不是最大的
    if (iMax != index) {
      swap(arr, iMax, index);
      index = iMax;
    }
  } while (iMax != index)
}
var swap = function(arr, i, j) {
  var temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}
複製程式碼

輕鬆實現建立大頂堆

建立大頂堆(Build-Max-Heap)的作用是,將一個陣列改造成一個大頂堆,接受陣列和堆大小兩個引數,其中會自下而上地呼叫 Max-Heapify 來改造陣列。

因為大頂堆調整(Max-Heapify)能夠保證下標為 i 的節點之後的節點都滿足大頂堆的性質,所以我們要自下而上地呼叫大頂堆調整(Max-Heapify)。

若最大頂堆的元素總數量為 n,則建立大頂堆(Build-Max-Heap)從下標為 getParentPos(n) 處開始,往上依次呼叫大頂堆調整(Max-Heapify)。過程如下圖所示:

建立大頂堆過程示意圖

建立大頂堆過程示意圖

演算法實現如下:

var buildMaxHeap = function(arr, heapSize) {
  var i, iParent = Math.floor((heapSize - 1) / 2);

  for (i=iParent; i>=0; i--) {
    maxHeapify(arr, i, heapSize);
  }
}
複製程式碼

輕鬆實現堆排序

堆排序(Heap-Sort)是堆排序的介面演算法,其先要呼叫建立大頂堆(Build-Max-Heap)將陣列改造為大頂堆;然後進入迭代,迭代中先將堆頂與堆底元素交換,並將堆長度縮短,繼而重新呼叫大頂堆調整(Max-Heapify)保持大頂堆性質。

因為堆頂元素必然是堆中最大的元素,所以每一次操作之後,堆中存在的最大元素會被分離出堆,重複 n-1 次周,陣列排序完成。過程如下圖所示:

堆排序過程示意圖

堆排序過程示意圖

演算法實現如下:

var heapSort = function(arr, heapSize){
  var i;

  buildMaxHeap(arr, heapSize);
  for (i=heapSize-1; i>0; i--) {
    swap(arr, 0, i);
    maxHeapify(arr, 0, i);
  }
};
複製程式碼

完整實現

綜合以上 3 塊程式碼,完整的 js 程式碼如下:

/* 大頂堆排序 */
var heapSort = function(arr, heapSize){
  var i;

  buildMaxHeap(arr, heapSize);
  for (i=heapSize-1; i>0; i--) {
    swap(arr, 0, i);
    maxHeapify(arr, 0, i);
  }
};

/* 建立大頂堆 */
var buildMaxHeap = function(arr, heapSize) {
  var i, iParent = Math.floor((heapSize - 1) / 2);

  for (i=iParent; i>=0; i--) {
    maxHeapify(arr, i, heapSize);
  }
};

/* 大頂堆調整 */
var maxHeapify = function(arr, index, heapSize) {
  var iMax, iLeft, iRight;
  do {
    iMax = index;
    iLeft = 2 * index + 1;
    iRight = 2 * (index + 1);

    // 是否左子節點比當前節點的值更大
    if (iLeft < heapSize && arr[index] < arr[iLeft]) {
      iMax = iLeft;
    }
    // 是否右子節點比當前更大節點的值更大
    if (iRight < heapSize && arr[iMax] < arr[iRight]) {
      iMax = iRight;
    }

    // 如果三者中,當前節點值不是最大的
    if (iMax != index) {
      swap(arr, iMax, index);
      index = iMax;
    }
  } while (iMax != index)
}
var swap = function(arr, i, j) {
  var temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}
複製程式碼

複雜度分析

堆排序的執行時間主要是消耗在初始構建堆和重建堆時的反覆篩選上。

我們這裡不深入探討演算法的時間複雜度計算,總體來說,堆排序的時間複雜度為 O(n*logn)。由於堆排序對原始陣列的排序狀態並不敏感,因此它無論最好、最壞還是平均時間複雜都為 O(n*logn)。這在效能上顯然要優於冒泡、簡單選擇、直接插入等複雜度為 O(n^2) 的演算法了。

另外,由於初始化構建堆所需的比較次數較多,因此它並不適合元素個數較少的陣列。

參考連結

bubkoo.com/2014/01/14/…

zh.wikipedia.org/wiki/%E5%A0…


微信公眾號

覺得本文不錯的話,分享一下給小夥伴吧~

相關文章