排序算法:堆排序的理解與實現

前言

堆排序相比冒泡排序、選擇排序、插入排序而言,排序效率是最高的,本文從堆的屬性和特色出發採用圖文形式進行講解並用JavaScript將其實現,歡迎各位感興趣的開發者閱讀本文😝javascript

堆屬性

堆分爲兩種: 最大堆和最小堆,二者的差異在於節點的排序方式。java

在不一樣類型的堆中,每個節點都遵循堆的屬性,下方所述內容即爲堆的屬性。api

  • 最大堆: 父節點的值大於子節點的值
  • 最小堆: 父節點的值小於子節點的值

由一個徹底二叉樹組成,且樹中的全部節點都知足堆屬性,這個徹底二叉樹就是堆。數組

根據堆的屬性可知:數據結構

堆的根節點中存放的是最大或最小元素,可是其餘節點的排列順序是未知的。 例如,在一個最大堆中,最大的那一個元素老是位於index0的位置,可是最小的元素則未必是最後一個元素,惟一可以保證的是最小的元素是一個葉節點,可是不肯定是哪個。函數

  • 最大堆根節點中的元素必定是樹中的最大值
  • 最小堆根節點中的元素必定是樹中的最小值

數組實現堆

用數組來實現堆,堆中的節點在數組的位置與它的父節點以及子節點的索引之間有一個映射關係。post

用公式來描述當前節點的父節點和子節點在數組中的位置(i爲當前節點的索引)測試

// 父節點的位置,向下取整(floor)
parent(i) = floor( i - 1 ) / 2)
// 左子節點的位置
left(i) = 2i +1
// 右子節點的位置,左右節點老是處於相鄰的位置
right(i) = left(i)+1 = 2i+2
複製代碼
  • 若計算出的索引不是一個有效的數組索引(小於0時),則當前節點沒有父節點
  • 若計算出的索引大於數組的長度,則當前節點沒有子節點。
  • 父節點的值必定大於等於其子節點的值,即:
array[parent(i)] >= array[i]
複製代碼
  • 當前層級全部的節點未填滿以前不容許開始下一層的填充

堆屬性計算

堆是一個徹底二叉樹,樹的高度是指從樹的根節點到最低葉節點所須要的步數。ui

樹高度正式的定義: 節點之間的邊的最大值。spa

  • 一個高度爲h的堆有h+1層。

  • 若是一個堆有n個節點,那麼它的高度爲 floor(log2(n))

// 例如,一個堆有15個節點,求這個堆的高度
h = floor(log2(15)) = floor(3.91) = 3
套入公式可得,堆的高度爲3
// log2(n),log爲求次方運算,log以2爲底,15的對數。若n爲8,則運算結果爲3,即2^3=8
複製代碼
  • 若是最下面一層已經填滿,那這一層的節點爲 2^h個。
// 高度爲3,即最下面一層有8個節點
2^3 = 8;
複製代碼
  • 最下面已經填滿的那一層以上的全部節點數目爲 2^h -1個。
// 高度爲3,即其餘層的全部節點有7個
2^3 -1 = 7;
複製代碼
  • 整個堆中的節點數爲: 2^(h+1)-1
// 高度爲3,即堆中的節點數爲15
2^4 -1 = 16 - 1 = 15;
複製代碼
  • 葉結點老是位於數組的floor(n/2)和n-1之間

用JS實現堆排序

實現堆排序以前,咱們須要先將即將排序的數據構建成一個最大堆,構建完成後,根據最大堆的屬性可知,堆頂部的值最大,咱們將它取出,而後從新構建堆,直到堆中的全部數據被取出,堆排序也就完成了。

調整徹底二叉樹中的樹爲一個最大堆

在一個徹底二叉樹中,從一個節點出發,找到它的左子樹和右子樹,將當前節點與它的兩顆子樹進行大小比較,找到兩顆樹中較大的一方,將其與當前節點進行交換,交換完畢後,當前節點所在的樹就是一個最大堆。咱們稱這個交換操做爲heapify

接下來,咱們來整理下實現思路

  • 聲明一個函數,參數爲: 樹,樹的節點數,當前要進行heapify操做的節點
  • 根據數組實現堆中所講的,父節點和子節點在數組中位置的計算公式,找到當前節點的左子樹和右子樹的位置
  • 假設最大值爲當前要操做的節點,將最大值與左子樹和右子樹分別進行大小比較
  • 進行大小比較後,若是最大值的位置不是當前操做節點的位置,則將其與當前操做節點位置的數據進行互換,遞歸調用heapify函數
  • 退出遞歸: 當前操做的節點大於等於樹的總節點數時,則退出遞歸。
  • 當前節點與左子樹和右子樹進行大小比較時,若是左、右子樹的位置大於樹的總節點數時,不由笑最大值賦值。

接下來咱們將上述思路轉化爲代碼:

/* * 1. 從一個節點出發 * 2. 從它的左子樹和右子樹中選擇一個較大值 * 3. 將較大值與這個節點進行位置交換 * 上述步驟,就是一次heapify的操做 * */

// n爲樹的節點數,i爲當前操做的節點 (找到這顆樹裏的最大節點)
const heapify = function (tree, n, i) {
    if(i >= n){
        // 結束遞歸
        return;
    }
    // 找到左子樹的位置
    let leftNode = 2 * i + 1;
    // 找到右子樹的位置
    let rightNode = 2 * i +2;

    /* 1. 找到左子樹和右子樹位置後,必須確保它小於樹的總節點數 2. 已知當前節點與它的左子樹與右子樹的位置,找到最大值 */
    // 設最大值的位置爲i
    let max = i;
    // 若是左子樹的值大於當前節點的值則最大值的位置就爲左子樹的位置
    if(leftNode < n && tree[leftNode] > tree[max]){
        max = leftNode;
    }
    // 若是右子樹的值大於當前節點的值則最大值的位置就爲右子樹的位置
    if(rightNode < n && tree[rightNode] > tree[max]){
        max = rightNode;
    }

    /* * 1. 進行大小比較後,若是最大值的位置不是剛開始設的i,則將最大值與當前節點進行位置互換 * */
    if(max !== i){
        // 交換位置
        swap(tree,max,i);
        // 遞歸調用,繼續進行heapify操做
        heapify(tree,n,max)
    }
};

// 交換數組位置函數
const swap = function (arr,max,i) {
    [arr[max],arr[i]] = [arr[i],arr[max]];
};

複製代碼

接下來咱們測試下heapify函數

const dataArr = [23,15,34,11,23,4,19,80];
// 咱們假設當前操做節點爲數組的0號元素,咱們對0號元素進行一次heapify才作
heapify(dataArr,dataArr.length,0);
// 打印結果
console.log(dataArr);
複製代碼

執行結果以下,觀察執行結果,咱們發現,0號元素所在的樹符合最大堆的屬性

將亂序數據構建成一個堆

一般狀況下,咱們的數據是亂序的,沒有規律可言,此時咱們就須要將這些數據構建成堆,heapify實現堆的構建前提是:知道當前操做節點的位置,此時咱們從數據的最後一個節點的父節點出發,進行heapify操做,直至當前操做節點爲數組的0號元素時,那麼這組數據就成了一個最大堆。

接下來,咱們整理下實現思路:

  • 找到樹的最後一個節點
  • 根據數組實現堆中所講的,尋找父節點位置的公式,根據公式咱們就能夠找到當前操做節點的父節點
  • 從樹最後一個節點的父節點開始進行heapify操做,直至當前操做節點爲0

接下來,咱們將上述思路轉化爲代碼:

/* * 將徹底二叉樹構建成堆 * 1. 從樹的最後一個父節點開始進行heapify操做 * 2. 樹的最後一個父節點 = 樹的最後一個子結點的父節點 * */
const buildHeap = function (tree,n) {
    // 最後一個節點的位置 = 數組的長度-1
    const lastNode = n -1;
    // 最後一個節點的父節點
    const parentNode = Math.floor((lastNode - 1) / 2);
    // 從最後一個父節點開始進行heapify操做
    for (let i = parentNode; i  >= 0; i--){
        heapify(tree, n, i);
    }
};
複製代碼

接下來咱們測試下buildHeap函數

const dataArr = [23,15,34,11,23,4,19,80];
buildHeap(dataArr,dataArr.length);
console.log(dataArr);
複製代碼

觀察執行結果,咱們發現數組中的數據已經知足最大堆的屬性

實現堆排序

咱們將最大堆構建完成後,根據最大堆的特性可知:堆的頂點爲這個堆的最大值,咱們將這個值取出,而後將堆的最後一個節點移動至堆的頂部,而後調用heapify,從新構建堆,直至最大堆中的數據所有被取出則排序完成。

接下來,咱們整理下實現思路:

  • 將數據先構建成一個最大堆
  • 從堆的最後一個節點出發,將其與樹的根節點進行位置交換
  • 交換完畢後,調用heapify從新調整堆。
  • 排序好一個數後,咱們的數組長度就會-1,則調用swapheapify時,樹的高度就是當前循環到的i的值。

接下來,咱們將上述思路轉化爲代碼:

// 堆排序函數
const heapSort = function (tree,n) {
    // 構建堆
    buildHeap(tree,n);
    // 從最後一個節點出發
    for(let i = n - 1; i >= 0; i--){
        // 交換根節點和最後一個節點的位置
        swap(tree,i,0);
        // 從新調整堆
        heapify(tree,i,0);
    }
};
複製代碼

接下來咱們測試下heapSort函數

const dataArr = [23,15,34,11,23,4,19,80];
heapSort(dataArr,dataArr.length);
console.log(dataArr);
複製代碼

觀察執行結果,咱們發現數組中的數據已經按照從小到大進行排列

寫在最後

  • 對堆不瞭解的開發者,能夠閱讀個人另外一篇文章:數據結構:堆
  • 文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊和關注😊
  • 本文首發於掘金,未經許可禁止轉載💌
相關文章
相關標籤/搜索