【算法】堆排序學習筆記

參考資料算法

 
《算法(第4版)》          — — Robert Sedgewick, Kevin Wayne

什麼是二叉堆

 
在瞭解堆排序以前, 最重要的固然是理解二叉堆的概念。 若是咱們從零開始探究這個問題:什麼是二叉堆呢? 那麼這就變成了一個有趣而冗長的一個問題了:
 

二叉堆:一個堆有序徹底二叉樹, 叫作二叉堆。後端

 
在瞭解二叉堆前, 先讓咱們理解下「堆有序」 和「徹底二叉樹」的概念
 

徹底二叉樹

 
讓咱們先從二叉樹談起
 
二叉樹
從一個根節點開始, 每一個節點鏈接一到兩個子節點,依次向下展開的樹形結構,叫作二叉樹。
 
滿二叉樹
在二叉樹的基礎上, 除了最後一層節點沒有任何子節點外,每一層的節點都有兩個子節點,且每一層都徹底填滿的二叉樹,叫作滿二叉樹。在外形上看,就像是一個完整的金字塔的形狀。(從深度和節點數的關係上看,一顆深度爲k且有2^k-1個節點的二叉樹稱爲滿二叉樹
 

 

徹底二叉樹
對滿二叉樹進行從上至下,從左至右的編號(例如上圖所示的從1到7)。若是一個深度爲k,有n個節點的二叉樹,其每一個節點都和深度同爲k的滿二叉樹的編號1到n的節點在位置上一一對應的話,這個二叉樹,就是徹底二叉樹。
 
 
 

 

做爲對比, 看看下圖中左下角和右下角的兩顆樹, 由於按照滿二叉樹的編號排定方式,它們相比起同深度的滿二叉樹而言, 分別在6和3的位置沒有對應的節點,因此不是徹底二叉樹
 
 
 

 

堆有序

 
對於一個二叉樹,若是它知足:
1. 任意一個父節點都小於等於它相鄰的全部子節點
2. 任意一個父節點都大於等於它相鄰的全部子節點
 
這兩種狀況均可稱之爲堆有序(和二叉搜索樹不一樣的是,二叉堆的左兒子和右兒子間沒有大小關係
 
 
堆有序徹底二叉樹, 就叫作二叉堆。
 
 

 

 
【注意】 在接下來的代碼示例中,咱們都將默認採起父節點都大於等於它的全部子節點的二叉堆做爲展現
 
 

二叉堆的表示方法

二叉堆能夠用一個普通的一維數組來表示
 
按照從上至下,從左至右的編號,把二叉堆節點從1編號到N,以此做爲數組下標,放入一個長度爲N+1的數組中。那麼就造成了一個用數組表示的二叉堆。例以下面的數組a
 
要注意的是, a[0]沒有存聽任何值! 數組是從a[1]開始存放的。
 
 
 

 

二叉堆節點間的位置關係

見上圖圖示, 二叉堆的節點位置(對應數組元素下標)間是有數字上的關係的:
在一個堆中,位置k的節點的父節點的位置是k/2, 而兩個子節點的位置則分別是2k和2k+1。這樣的話,在該二叉堆對應的數組中,咱們就能夠經過計算數組的下標在堆中上下移動: 從a[k]向上一層就令k等於k/2,向下一層則令k等於2k或者2k+1
 
也就是說,經過這層數字關係, 咱們能夠找到任意一個堆中節點的父節點和子節點(若是有的話),並進行比較, 在這層基礎上,咱們就能夠來設計咱們的堆有序化的算法了。
 

堆有序化的基本算法:上浮和下沉

堆排序的核心,就是堆有序化的算法
而堆有序化的基礎,就是針對單個堆節點的有序化
單個堆節點的有序化有兩種狀況:
  1. 當某個節點變得比它的父節點更大而被打破(或是在堆底加入一個新元素時候),咱們須要由下至上恢復堆的順序
  2. 當某個節點的優先級降低(例如,將根節點替換爲一個較小的元素)時,咱們須要由上至下恢復堆的順序
實現這兩種有序化的操做,分別叫作「上浮」(swim)和「下沉」 (sink)
 
基於上面所述,下面咱們討論的原始場景是這樣的: 只有一個堆節點是非有序的,而其餘堆節點都是有序的。
在這種前提下, 該節點對整體堆的順序破環性只有兩種狀況:
  1. 該節點比它的父節點大
  2. 該節點比它的其中一個兒子節點小
(不可能既比父節點大又比兒子節點小,由於咱們假設了前提: 其餘節點都是堆有序的
 

上浮實現堆有序

 
對第一種狀況:某個節點比它的父節點大,以下圖中位置5上的節點
(在圖中,節點爲從A到Z的字母,順序上A最小Z最大)
 

 

 
這個時候咱們要作的就是:
1.交換它和父節點(位置2)的值, 即P和T互換(這時P將處在5的位置,由於咱們假設了前提「其餘堆節點都是有序的」,因此互換位置後P確定是有序的,不用考慮P的順序問題)
2. 基於1, 咱們須要判斷交換後T和新的父節點(S)的大小關係, 若是仍是大於父節點,那麼再次交換。 不斷循環,直到小於父節點或者該節點已經位於根節點爲止(最終T到達了根節點的位置)
 
下面是咱們的上浮方法(和圖示的字母不一樣,咱們的節點優先級是用整型數值的大小表示的)
  /**
   * @param a 表示堆的數組
   * @param k 堆中的節點位置
   */
  private void swim (int [] a, int k) {
    while(k>1&&a[k]>a[k/2]){ // 當該結點存在父節點,且大於父節點的時候
    exchange(a, k, k/2); // 交換它和它的父節點值
    k = k/2; // 取得父節點的位置
    }
  }

 

 

下沉實現堆有序

 
 
對第二種狀況:某個節點比它的至少一個兒子節點小,以下圖中位置2上的節點
(在圖中,節點爲從A到Z的字母,順序上A最小Z最大)
 

 

 
這個時候咱們要作的就是:
1. 比較該節點(H)的兩個子節點的大小, 取得那個比較大的子節點的值(圖中S),和該節點交換。 成爲父節點的那個較大的子節點, 同時大於原子節點(P)和新的子節點(原父節點H),三個節點間是堆有序的
2. 和1同樣,再次判斷交換後的該節點和新的子節點(N和G)的關係, 不斷循環, 直到大於兩個子節點或者到達了底部的葉子節點爲止。
 
下面是咱們的下沉方法:
  /**
   * @param a 表示堆的數組
   * @param k 堆中的節點位置
   * @param N 堆中節點總數
   */
  private void sink (int [] a, int k, int N) {
    while(2*k<=N) { // 當該節點存在至少一個子節點的時候
      int j = 2*k;  // 取得左兒子的位置
      if(j<N&&a[j]<a[j+1]) { j++; } // 取得左兒子和右兒子中的較大者
      if(a[k]<a[j]) {  // 當該節點的值小於較大的左兒子的時候
      exchange(a, k, j); // 交換它和該兒子節點的值
      k = j; // 取得該兒子節點的位置
      }
      else { break; }
    }
  }

 

 
 

二叉堆的基本操做:插入元素和刪除最大元素

 
爲了更好地理解堆排序的過程,讓咱們利用剛纔的上浮和下沉方法,實現一下對二叉堆的插入和刪除最大元素這兩個操做。
 
在這以前要提一下的是,咱們把上面的兩個方法:sink (int [] a, int k, int N) 和swim (int [] a, int k)中的N和k提取出來變成Heap類的全局變量,這樣的話, 這兩個方法就簡化成了sink (int k) 和swim (int k)
 

向堆中插入元素

咱們將新元素加到數組末尾, 增長堆的大小並讓這個新元素上浮到合適的位置
 
如圖所示
 

 

 
代碼:
  /**
   * 向堆中插入值
   * @param v 要插入的值
   */
  public void insert (int v) {
    a[N+1]= v;  // 元素被放入堆的最末端成爲新節點
    N++;  // 增長堆的大小
    swim(N); // 對末端節點進行上浮操做使其有序
  }

 

 

向堆中刪除最大元素

咱們從數組頂端刪除最大的元素並將數組的最後一個元素放到頂端, 減少堆的大小並讓這個元素下沉到合適的位置
如圖所示:
 

 

 
代碼以下:
  /**
   * 刪除堆中的最大值, 而且將其返回
   * @return 堆節點最大值
   */
  public int delMax () {
    if(isEmpty()) return 0; // 當堆爲空時, 返回
    int max = a[1];  //  取得堆中根節點(最大值)
    exchange(a, 1, N);   // 交換根節點和末端節點(最後一個元素)的值
    N--;  // 減小堆的大小 (「刪除」完畢)
    sink(1); // 下沉操做,讓剛放上根節點的新元素下沉到合適的位置
    return max;
  }

 

 
Heap類的所有代碼:
public class Heap {
  /**
   *  N: 記錄二叉堆中的節點總數
   *  a:  容納二叉堆節點的數組,從a[0]開始存放
   */
  private int N = 0;
  private int [] a;
  /**
   * @param maxN 建立堆中節點的總數
   */
  public Heap (int maxN) {
    a = new int [maxN+1];
  }
 
  private static void exchange(int [] a , int i, int j) {
    int temp = a[i];
    a[i] = a[j];
    a[j] = temp;
  }
  /**
   * 上浮操做
   * @param k 堆中的節點位置
   */
  private void swim (int k) {
    while(k>1&&a[k]>a[k/2]){ // 當該結點存在父節點,且大於父節點的時候
      exchange(a, k, k/2); // 交換它和它的父節點值
      k = k/2; // 取得父節點的位置
    }
  }
  /**
   * 下沉操做
   * @param k 堆中的節點位置
   */
  private void sink ( int k ) {
    while(2*k<=N) { // 當該節點存在至少一個子節點的時候
      int j = 2*k;  // 取得左兒子的位置
      if(j<N&&a[j]<a[j+1]) { j++; } // 取得左兒子和右兒子中的較大者
      if(a[k]<a[j]) {  // 當該節點的值小於較大的左兒子的時候
        exchange(a, k, j); // 交換它和該兒子節點的值
        k = j; // 取得該兒子節點的位置
      }
      else { break; }
    }
  }
 
  /**
   * 向堆中插入值
   * @param v 要插入的值
   */
  public void insert (int v) {
    a[N+1]= v;  // 元素被放入堆的最末端成爲新節點
    N++;  // 增長堆的大小
    swim(N); // 對末端節點進行上浮操做使其有序
  }
 
  /**
   * 刪除堆中的最大值, 而且將其返回
   * @return 堆節點最大值
   */
  public int delMax () {
    if(isEmpty()) return 0; // 當堆爲空時, 返回
    int max = a[1];  //  取得堆中根節點(最大值)
    exchange(a, 1, N);   // 交換根節點和末端節點(最後一個元素)的值
    N--;  // 減小堆的大小 (「刪除」完畢)
    sink(1); // 下沉操做,讓剛放上根節點的新元素下沉到合適的位置
    return max;
  }
 
  /**
   * 判斷堆數組是否爲空
   */
  public boolean isEmpty() {
    return N == 0;
  }
 
}

 

 
測試代碼:
 
public class Test {
  public static void main (String [] args) {   
// 建立一個能容納10個元素的堆 Heap heap = new Heap(10); int [] array =  {2,6,3,9,1,5,4,3,0,2}; // 將數組元素依次放入堆中 for(int i =0; i<array.length;i++) { heap.insert(array[i]); } // 依次刪除堆中的最大元素並輸出 for(int i =0; i<array.length;i++) { System.out.println(heap.delMax()); }   } }

 

輸出:
9
6
5
4
3
3
2
2
1
0

 

 
誒.仔細看一下咱們有序的輸出,是否是找到了些「排序」的影子呢?
咱們上面所學習的堆的基本操做之一——刪除最大元素,使用的Heap.delMax方法的有趣之處在於:每次調用它,都會
 
1.刪除最大元素
2.返回該最大元素
3.使得剩下的堆中元素從新恢復有序
 
這樣一來,依次刪除返回的就是最大值,第二大值,第三大值...這樣一來不就獲得了有序的數組了嗎?  下面就讓咱們來看看堆排序
 

堆排序

 
堆排序分爲兩個階段:1.堆的構造 2.下沉排序
 
在堆的構造階段,咱們把原始數組從新組織安排進一個堆中,也就是讓這個數組實現堆有序(注意「數組堆有序」 ≠ 「數組有序」!!)
而在下沉排序階段,咱們從堆中按遞減順序取得全部元素並獲得排序結果。 (相似Heap.delMax)
 

堆的構造階段

根據前面所講的知識,咱們能夠猜想, 將堆無序的數組變成堆有序的數組能夠有由兩種不一樣的操做完成: 第一種是使用「上浮」實現堆有序, 第二種是使用「下沉」實現堆有序。
 
而這兩種方式的效率是不同的: 由於「下沉」須要遍歷的節點數比「上浮」須要遍歷的節點數少了一半
 
1.對上浮排序,咱們須要對數組從左向右(或者說對堆從上到下)進行遍歷, 依次保證前兩個元素堆有序, 前三個元素堆有序...... 直到最後一個元素,如圖:
 

 

 
2.而「下沉」實現排序的話,咱們須要對數組從右向左(或者說對堆從下到上,但不是最下而是中間)進行遍歷
 

 

 
可是! 由於最下方的葉子節點沒有子節點,因此不須要排序! 而需要排序的節點(有子節點的節點)佔N/2 (假設N爲節點總數的話), 因此以下圖所示:
 
因此說,下沉什麼的最棒了!!!
 
接下來咱們的堆排序是如下沉(sink)爲基礎進行的, 有興趣的話你們也能夠用上浮實現一下哦
 
堆的構造代碼:
int N = a.length;  // 取得節點總數
for(int i=N/2;i>0; i--) {
  sink(a, i, N); // 對全部父節點,從最後一個父節點到根節點,依次下沉排序

 

【注意】不要寫成int N = a.length-1; 由於雖然咱們以前寫的二叉堆是忽略了a[0]的(堆節點數N = a.length - 1 ),但這是待排序的數組,固然不能忽略a[0]; (堆節點數N = a.length)
 
 
 
圖示以下:
 
 

 

把構造完畢(實現堆有序)以後,咱們就要將「堆有序」的數組轉化爲「有序」的數組,這一階段被稱爲下沉排序數組

下沉排序階段

 
依次把最大的數組元素移到數組的最右端, 依次填充a[N-1], a[N-2]...直到a[0]
    while(N>1){
      exchange(a, 1, N); // 將數組中最大的元素放到數組後端
      N--; // 將最大的節點元素移出堆
      sink(a, 1, N); // 下沉操做,再次實現堆有序
    }

 

如圖所示:less

  

 

 
兩個階段的總代碼:
  /**
    * 堆排序方法
    * @param a 待排序數組
    */
  public static void sort(int [] a) {
    // 堆的構造階段
    int N = a.length;  // 取得節點總數
    for(int i=N/2;i>0; i--) {
      sink(a, i, N); // 對全部父節點,從最後一個父節點到根節點,依次下沉排序
    }  // 到這裏數組已經徹底堆有序
   
// 下沉排序階段     while(N>1){      exchange(a, 1, N); // 將數組中最大的元素放到數組後端      N--; // 將最大的節點元素移出堆     sink(a, 1, N); // 下沉操做,再次實現堆有序     }   }

 

 
最後要說明一下,在堆排序中,咱們的exchange方法和less方法要減1, 由於相比起二叉堆的使用中忽略了a[0], 而實際須要排序的數組固然是有a[0]的呀:
  /**
    * 交換兩個數組元素的值
    * 注意! 不一樣於通常的exchange, 這裏的i和j要減1!
    */
  private static void exchange(int [] a , int i, int j) {
    int temp = a[i-1];
    a[i-1] = a[j-1];
    a[j-1] = temp;
  }
  /**
   * 比較i和j下標的數組元素的大小
   * 注意! 不一樣於通常的less, 這裏的i和j要減1!
   */
  private static boolean less (int [] a, int i, int j) {
    return a[i-1]-a[j-1]<0 ? true : false;
  }

 

 
堆排序類HeapSort的所有代碼:
public class HeapSort {
 
  /**
    * 交換兩個數組元素的值
    * 注意! 不一樣於通常的exchange, 這裏的i和j要減1!
    */
  private static void exchange(int [] a , int i, int j) {
    int temp = a[i-1];
    a[i-1] = a[j-1];
    a[j-1] = temp;
  }
  /**
   * 比較i和j下標的數組元素的大小
   * 注意! 不一樣於通常的less, 這裏的i和j要減1!
   */
  private static boolean less (int [] a, int i, int j) {
    return a[i-1]-a[j-1]<0 ? true : false;
  }
  /**
    * 下沉操做
    * @param a 待排序數組
    * @param k 堆中的節點位置
    * @param N 堆中的節點總數
    */
  private static void sink (int [] a, int k, int N) {
    while(2*k<=N) { // 當該節點存在至少一個子節點的時候
      int j = 2*k;  // 取得左兒子的位置
      if(j<N&&less(a, j, j+1)) { j++; } // 取得左兒子和右兒子中的較大者
      if(less(a, k, j)) {  // 當該節點的值小於較大的左兒子的時候
        exchange(a, k, j); // 交換它和該兒子節點的值
        k = j; // 取得該兒子節點的位置
      }
      else { break; }
   }
  }
 
  /**
    * 堆排序方法
    * @param a 待排序數組
    */
  public static void sort(int [] a) {
    // 堆的構造階段
    int N = a.length;  // 取得節點總數
    for(int i=N/2;i>0; i--) {
      sink(a, i, N); // 對全部父節點,從最後一個父節點到根節點,依次下沉排序
    }  // 到這裏數組已經徹底堆有序
    // 下沉排序階段
    while(N>1){
      exchange(a, 1, N); // 將數組中最大的元素放到數組後端
      N--; // 將最大的節點元素移出堆
      sink(a, 1, N); // 下沉操做,再次實現堆有序
    }
  }
}

 

 
測試代碼:
public class Test {
  public static void main (String [] args) {
    int [] array =  {3,0,8,9,1,5,4,2,7,1,2};
    HeapSort.sort(array);
    for(int i=0;i<array.length;i++) {
      System.out.println(array[i]);
    }
  }
}

 

 
輸出:
0
1
1
2
2
3
4
5
7
8
9

 

相關文章
相關標籤/搜索