圖解:什麼是堆排序?

2021-02-18 景禹

二叉堆(Binary Heap)是一顆特殊的完全二叉樹,一般分為大頂堆和小頂堆,我就不囉嗦啦!具體內容你可以看一下 圖解:什麼是二叉堆?

堆排序

要學習今天的堆排序(Heap Sort),我們以一個數組  arr = [5、1、4、2、8、4] 開始(這個數組我們之前講排序算法常用的):

我們首先以這個數組建立一個大頂堆,插入結點 5 作為根結點:

然後將結點 1 插入到最後一個位置,也就是結點 5 的左孩子,1 < 5 ,滿足大頂堆的屬性:

將結點 4 插入到最後一個位置,即結點 5 的右孩子 ,又因為 4 < 5 ,滿足大頂堆的屬性,不需要進行調整:

將結點 2 插入到最後一個位置,即結點 1 的左孩子位置,但是此時不滿足大頂堆的屬性(插入結點 2 小於其父結點 1 的值),所以交換兩者的值;此時並未結束,繼續判斷此時插入結點 2 與當前父結點 5 的大小關係,發現 2 < 5 ,滿足大頂堆的屬性,結束判斷。這個過程就是二叉堆的插入操作:

緊接著將結點 8 插入到最後一個位置,即結點 2 的右孩子位置,此時不滿足大頂堆的屬性(插入結點 8 小於其父結點 2 ),故交換兩者位置;然後繼續向上修正,判斷當前結點 8 與其父結點 5 的大小關係,8 > 5 (不滿足大頂堆的屬性),交換兩者位置,繼續修正,發現結點 8 已為樹的根結點,修正結束:

最後,我們將結點 4 插入到最後一個位置,即結點 4(下標為2) 的左孩子位置,且其值小於等於父結點,故不進行修正:

以上算是對於二叉堆插入操作的一個回顧,建堆的過程(這裡是按照插入操作進行建堆的),接下來才是堆排序的核心操作。

arr = [8,5,4,1,2,4] 而言,

第一步:將堆頂的元素 8 (即數組 arr[0] ,最大元素)的元素與堆的最後一個元素 4(即數組當中的最後一個元素 4 )交換,此時就相當於選擇出了數組當中的最大元素,將其從堆中去掉:

第二步:從結點 4 (下標為 0 )開始進行 堆化(Heapify)操作,這裡我們只囉嗦一次奧!計算結點 4 (下標為 0 )的左孩子 5 ),右孩子 4),比較三者的大小,發現 5 > 4 違反了堆的性質,交換 5 和根結點 4 ;然後繼續對結點 4 (下標為 1 )進行判斷,發現其左孩子 1 和右孩子 2 均小於 4 ,堆化結束。

第三步:將堆頂元素 5 (下標為 0)和當前最後一個元素 2 (即 i 指向的位置)交換, 此時就相當於選擇出了數組當中的次最大元素,將其從堆中去掉:

第四步:從當前的堆頂元素 2 開始進行堆化操作,交換 2 (下標 0)和其左孩子 4 (下標 1),為什麼不是右孩子 4 (下標 2)呢?因為我們在堆化的時候,優先和左孩子進行了對比,只有當右孩子大於左孩子的情況下,才考慮將右孩子與其父結點交換,堆化後的結果如下圖所示:

第五步:交換根結點 4 和最後一個結點 1 ,從堆中去掉結點 4 (下標 3):

第六步:從根結點 1 開始進行堆化操作,交換了根結點 14 (下標 2):

第七步:交換根結點 41 ,從堆中去掉結點 4

第八步:從根結點 1 開始進行堆化操作,交換了結點 21

第九步:交換根結點 2 和最後一個元素 1 ,將結點 2 從堆中去掉:

第十步:發現堆中僅剩餘一個元素,堆排序結束,我們看到原始的輸入數組   arr = [5、1、4、2、8、4]  變成了有序數組   arr = [1、2、4、4、5、8]  。

這就是有趣有好玩的堆排序,其本質上是對二叉堆的一個應用。

我們都知道選擇排序是利用線性的時間複雜度

而堆排序事實上就是對選擇排序的一個優化,本來用

不難發現,堆排序是一個基於比較的排序算法,且在排序過程中由於要進行堆化操作(不斷交換)(Heapify),而造成其不穩定性,所以堆排序是一個不穩定的排序算法。

實現代碼

只要會寫二叉堆的堆化操作,看堆排序的代碼會相當簡單。

public class HeapSort 

    public void heapSort(int arr[]) 
    { 
        int n = arr.length; 
  
        //建堆(你也可以考慮進行上面的插入操作)但是這裡調用的是Heapify
        //可以達到同樣的建堆效果
        for (int i = n / 2 - 1; i >= 0; i--){
         heapify(arr, n, i); 
        } 

        //從堆中一個一個地選擇出最大元素
        for (int i = n-1; i > 0; i--) 
        { 
            // 交換堆的根結點(最大元素)與當前最後一個元素(i)
            int temp = arr[0]; 
            arr[0] = arr[i]; 
            arr[i] = temp; 
  
            // 在去掉最後一個元素的堆上進行堆化操作
            heapify(arr, i, 0); 
        } 
    } 
  
    // 堆化操作
    void heapify(int arr[], int n, int i) 
    { 
        int largest = i; // 初始化最大元素為根結點
        int l = 2*i + 1; // i 的左孩子結點 left = 2*i + 1 
        int r = 2*i + 2; // i 的右孩子結點 right = 2*i + 2 
  
        // 如果左孩子結點比根結點大,更新largest為左孩子
        if (l < n && arr[l] > arr[largest]) 
            largest = l; 
  
        // 如果右孩子比最大元素大,更新largest為右孩子
        if (r < n && arr[r] > arr[largest]) 
            largest = r; 
  
        // 如果最大元素不是根結點,進行交換操作並遞歸調用Heapify
        if (largest != i) 
        { 
            int swap = arr[i]; 
            arr[i] = arr[largest]; 
            arr[largest] = swap; 
  
            // 對由於交換操作受到影響的子樹遞歸調用Heapify
            heapify(arr, n, largest); 
        } 
    } 
    public static void main(String args[]) 
    { 
        int arr[] = {5,1,4,2,8,4}; 
        int n = arr.length; 
  
        HeapSort ob = new HeapSort(); 
        ob.sort(arr); 
  
        for(int i = 0; i < n; i++){
            System.out.print(arr[i] + ",")
        }
    } 

請注意:上面代碼中的建堆操作代碼

for (int i = n / 2 - 1; i >= 0; i--){
    heapify(arr, n, i); 

如果看這個代碼感覺不舒服,沒關係,我們用更香的方式來一遍。我們還是以數組   arr = [5、1、4、2、8、4]  為例說明這種建堆方式。

與插入操作建堆不同的是(插入操作建堆將原數組當做一個普通的數組),我們將數組 arr[] 從一開始就當做一顆完全二叉樹:

然後計算 i = 6/2 - 1 = 2 ,對結點 4(2) 應用堆化操作,發現大於等於其左孩子 4(5) 的值(其中括號中的數字表示下標):

i-- ,i = 1 ,對結點 1(1) 應用堆化操作,計算其左孩子結點 2(3) ,右孩子結點 8(4) ,比較三者大小,發現結點  1(1) 的左右孩子均比其大,將最大結點 8(4) 和  1(1) 交換,此時   1(1) 已經到葉子結點了:

i-- = 0 ,對結點 5(0) 應用堆化操作,發現左孩子 8(3) 比其大,交換兩者,繼續對 5(3) 進行堆化,發現左右孩子均比其小,堆化結束:

這樣我們就得到了一個大頂堆。

for (int i = n / 2 - 1; i >= 0; i--){
    heapify(arr, n, i); 

一個更有意思的問題來了,那你知道剛才講的 建堆時間複雜度是多少呢?

咋一看,這還不簡單,每次調用 Heapify() 函數的時間複雜度為

雖然上面的建堆操作的時間複雜度的上限

Heapify() 函數的運行時間取決於樹的高度

建堆的循環是從倒數第一層的結點

要想準確計算出建堆的時間複雜度,就必須知道對於高度為

這裡就要告訴大家一個不爭的事實啦,對於一個大小為

那麼建堆的時間複雜度就好算了,對於高度為

已知:

那麼:

因此,建立一個二叉堆的時間複雜度為

證明建立一個二叉堆的時間複雜度對於學習堆排序似乎沒有特別的意義,但希望考研、學習高等數學的朋友看到數學的魅力,還有數據結構的算法複雜度細究其實還是很有趣的。

祝你們周末愉快!記得點讚奧

推薦閱讀:

圖解「歸併排序」算法(修訂版)

圖解:什麼是快速排序?

漫畫:什麼是計數排序?

作者:景禹,一個追求極致的共享主義者,想帶你一起擁有更美好的生活,化作你的一把傘。

相關焦點

  • 高手才能看懂的JAVA排序算法圖解
    背景排序算法是JAVA面試中經常被問到的面試題,但排序算法很多,全部記下是一件很困難的事情。現整理多種排序算法圖解幫助JAVA面試者理解和掌握排序算法,能看懂下面的算法圖解,說明你已經掌握了該排序算法。
  • 動畫 | 什麼是堆排序?
    而堆排序因為二叉堆的性質,堆頂就是最大的元素,查找次數只有一次,但是將無序轉成有序中間還需要一個預處理過程:構造堆有序。 堆有序並不代表數組有序,堆有序是滿足 二叉堆 性質的: 1.父節點的鍵值總是優先於任何一個子節點的鍵值; 2.左右子樹都是一個二叉堆。
  • 到底什麼是堆排序 PHP
    每個節點的值都大於等於子樹中每個節點值的堆叫最大堆,反之為最小堆。堆排序的時間複雜度為O(nlogn)。堆是一種特殊的二叉樹,它滿足以下兩點:1. 堆是一個完全二叉樹2. 堆中每個節點都必須大於等於(或小於等於) 其子樹的每個節點
  • 快速排序和堆排序
    龜哥今天和大家分享下快速排序,堆排序算法。首先快速排序的思想,給定一個無序的數組,首先用兩個指針left和right分別指向數組的頭部和尾部,將數組的第一個元素array[0]作為和left,right指針比較的元素。
  • 堆排序與拓撲排序(Java模板)
    1.堆排序思想(1) 以升序排序為例,我們需要構建一個小根堆。第一種方法:用數組模擬小根堆。忘記了的朋友可以看看之前的文章《堆模板(Java)》認真複習一下~第二種方法:使用 java.util.PriorityQueue ,相當於一個堆結構。
  • 堆排序原理及實現
    小白們也很熟悉的:冒泡,歸併,簡單選擇,歸併,堆排序。那它們區別是什麼呢?需要好好梳理一下了。今天咱們就先瞅瞅感覺比較抽象的堆算法吧。什麼是堆?堆是具有下列性質的完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱為大頂堆;或者每個結點的值都小於或等於其左右子結點的值,稱為小頂堆。
  • 詳解堆排序
    堆是一棵順序存儲的完全二叉樹。其中每個結點的關鍵字都不大於其孩子結點的關鍵字,這樣的堆稱為小根堆。其中每個結點的關鍵字都不小於其孩子結點的關鍵字,這樣的堆稱為大根堆。構造了初始堆後,我們來看一下完整的堆排序處理:還是針對前面提到的無序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 } 來加以說明。
  • 堆排序就這麼簡單
    一、堆排序介紹來源百度百科:堆排序(Heapsort)是指利用堆積樹(堆)這種數據結構所設計的一種排序算法,它是選擇排序的一種。可以利用數組的特點快速定位指定索引的元素。堆分為大根堆和小根堆,是完全二叉樹。前面我已經有二叉樹入門的文章了,當時講解的是二叉查找樹,那上面所說的完全二叉樹是怎麼樣的一種二叉樹呢??
  • 第二篇,選擇排序算法:簡單選擇排序、堆排序
    此為第二篇,選擇排序算法:簡單選擇排序、堆排序。
  • 堆排序算法
    堆是一種數據結構,一種叫做完全二叉樹的數據結構。堆是具有以下性質的完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱為大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱為小頂堆。大頂堆:a[i] >=a[2i+1] && ar[i] >= a[2i+2] 小頂堆:a[i] <= a[2i+1] && a[i] <= a[2i+2] 堆排序的基本思想:
  • 優先級隊列與堆排序
    如下圖,以對S O R T E X A M P L E 排序為例,首先本地構造一個最大堆,即對節點進行Sink操作,使其符合二叉堆的性質。然後再重複刪除根節點,也就是最大的元素,操作方法與之前的二叉堆的刪除元素類似。
  • 一文讀懂堆排序
    (點擊上方公眾號,可快速關注)轉自:劉毅https://61mon.com/index.php/archives/202/好文投稿, 請點擊 → 這裡了解詳情堆排序是利用堆的性質進行的一種選擇排序下面先討論一下堆。堆實際上是一棵完全二叉樹,其滿足性質:任何一結點大於等於或者小於等於其左右子樹結點。堆分為大頂堆和小頂堆,滿足 「任何一結點大於等於其左右子樹結點」 的稱為大頂堆,滿足 「任何一結點小於等於其左右子樹結點」 的稱為小頂堆。由上述性質可知:大頂堆的堆頂肯定是最大的,小頂堆的堆頂是最小的。
  • 八十四、堆排序解決TopK問題
    「@Author:Runsen」上次介紹了堆排序,這次介紹堆排序常見的應用場景TopK問題。利用堆求TopK問題TopK問題是一個堆排序典型的應用場景。至於為什麼TopK問題最佳的答案是堆排序?其實在空間和時間的複雜度來考量,雖說快排是最好的排序算法,但是對於100億個元素從大到小排序,然後輸出前 K 個元素值。可是,無論我們掌握的是快速排序算法還是堆排序算法,在排序的時候,都需要將全部的元素讀入到內存中。
  • 堆排序算法C語言詳解
    在學習堆排序之前,首先需要了解堆的含義:在含有 n 個元素的序列中,如果序列中的元素滿足下面其中一種關係時,此序列可以稱之為堆。
  • 排序算法之高效排序法
    什麼時候最快當輸入的數據可以均勻的分配到每一個桶中。什麼時候最慢當輸入的數據被分配到了同一個桶中。圖解桶排序桶排序中很重要的一步就是桶的設定了,我們必須根據輸入元素的情況,選擇一個恰當的 「arrIndex」 算法,使得輸入元素能夠正確的放入對應的桶內,且保證輸入數據能夠儘量均勻的放入不同的桶內。
  • 動畫:什麼是基數排序?
    基數排序 與基於比較的排序算法(歸併排序、堆排序、快速排序、冒泡排序、插入排序等等)相比,基於比較的排序算法的時間複雜度最好也就是 基數排序的總體思想就是從待排序數組當中,元素的最低有效位到最高有效位 逐位 進行比較排序;此外,基數排序使用計數排序作為一個排序的子過程。下面我們就以數組  [170, 45, 75, 90, 802, 24, 2, 66] ,快樂學習基數排序!
  • ...選擇,插入,希爾,歸併,快排,堆排序,計數排序,桶排序,基數排序)
    無論是什麼數據進去,時間複雜度都是O(n*2)。數據規模越小越好,唯一的好處就是不佔用額外的空間。思路:在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。然後再從剩餘未排序的數組匯總繼續尋找最小(大)元素,依次類推。
  • 圖解:什麼是拓撲排序?
    今天景禹給你們談一談拓撲排序,乍一聽上去,感覺很高大上,但她的確很高大上,所以一起徵服她吧!在正式介紹拓撲排序之前,我們一起看一看必備基礎。拓撲排序基礎篇 總覺得書上的概念有點欠缺生動,但還是需要這些基礎的概念作為支撐。
  • 一文圖解 Java 源碼的插入排序算法
    (一)》講了什麼是選擇問題,什麼是比較能力。那什麼是算法?算法是某種集合,是簡單指令的集合,是被指定的簡單指令集合。確定該算法重要的指標:第一是否能解決問題;第二算法運行時間,即解決問題出結果需要多少時間;還有所需的空間資源,比如內存等。很多時候,寫一個工作程序並不夠。因為遇到大數據下,運行時間就是一個重要的問題。算法性能用大 O 標記法表示。
  • 算法圖解 | 分而治之與快速排序算法
    快速排序例如快速排序問題,一個列表進行排序,如下圖首先選擇列表中的一個元素作為基準元素,其他的元素都與這個元素做比較,找出小於這個基準值的值、大於基準值的值。(O表示法表示)快速排序的性能高度依賴於你選擇的基準值。