數據結構與算法學習十八:堆排序

前言

一、堆排序基本介紹

  1. 堆排序是利用堆這種數據結構而設計的一種排序算法,堆排序是一種選擇排序,它的最壞,最好,平均時間複雜度均爲O(nlogn),它也是不穩定排序。

  2. 堆是具有以下性質的完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱爲 大頂堆, 注意 : 沒有要求結點的左孩子的值和右孩子的值的大小關係。

  3. 每個結點的值都小於或等於其左右孩子結點的值,稱爲 小頂堆

  4. 大頂堆舉例說明
    在這裏插入圖片描述
    我們對堆中的結點按層進行編號,映射到數組中就是下面這個樣子: (這裏其實就是順序儲存二叉樹
    在這裏插入圖片描述
    大頂堆特點:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] // i 對應第幾個節點,i從0開始編號

  5. 小頂堆舉例說明
    在這裏插入圖片描述
    小頂堆特點:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] // i 對應第幾個節點,i從0開始編號

  6. 一般從下往上: 升序採用大頂堆,降序採用小頂堆

二、堆排序基本思想

  • 堆排序的基本思想是:
  1. 將待排序序列構造成一個 大頂堆
  2. 此時,整個序列的 最大值就是堆頂的根節點
  3. 將其與末尾元素進行交換,此時末尾就爲最大值。
  4. 然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反覆執行,便能得到一個有序序列了。
  • 可以看到在構建大頂堆的過程中,元素的個數逐漸減少,最後就得到一個有序序列了.

三、思路圖解

要求:給你一個數組 {4,6,8,5,9} , 要求使用堆排序法,將數組升序排序。

3.1 步驟一 構造初始大頂堆。

將給定無序序列構造成一個大頂堆(一般升序採用大頂堆,降序採用小頂堆)。

  1. .假設給定無序序列結構如下
    在這裏插入圖片描述
  2. .此時我們從最後一個非葉子結點開始(葉結點自然不用調整,第一個非葉子結點 arr.length/2-1=5/2-1=1,也就是下面的6結點),從左至右,從下至上進行調整。
    在這裏插入圖片描述
  3. .找到第二個非葉節點4,由於[4,9,8]中9元素最大,4和9交換。
    在這裏插入圖片描述
  4. 這時,交換導致了子根[4,5,6]結構混亂,繼續調整,[4,5,6]中6最大,交換4和6。
    在這裏插入圖片描述

此時,我們就將一個無序序列構造成了一個大頂堆。

3.2 步驟二 將堆頂元素與末尾元素進行交換

將堆頂元素與末尾元素進行交換,使末尾元素最大。然後繼續調整堆,再將堆頂元素與末尾元素交換,得到第二大元素。如此反覆進行交換、重建、交換。

  1. .將堆頂元素9和末尾元素4進行交換
    在這裏插入圖片描述
  2. .重新調整結構,使其繼續滿足堆定義
    在這裏插入圖片描述
  3. .再將堆頂元素8與末尾元素5進行交換,得到第二大元素8.
    在這裏插入圖片描述
  4. 後續過程,繼續進行調整,交換,如此反覆進行,最終使得整個序列有序
    在這裏插入圖片描述

3.3 再簡單總結下堆排序的基本思路

  1. 將無序序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆;
  2. 將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
  3. 重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。

四、堆排序代碼實現

要求:給你一個數組 {4,6,8,5,9} , 要求使用堆排序法,將數組升序排序。
代碼實現:
說明:

  1. 堆排序不是很好理解,通過Debug 幫助大家理解堆排序
  2. 堆排序的速度非常快,在我的機器上 8百萬數據 3 秒左右。O(nlogn)

4.1 代碼實現

package com.feng.ch12_tree;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

/*
 * 堆排序
 * 堆 是具有以下性質的  完全二叉樹:
 * 1、每個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆(注意 : 沒有要求結點的左孩子的值和右孩子的值的大小關係。)
 * 2、每個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆
 *
 * 完全二叉樹:
 * 如果該二叉樹的所有葉子節點都在最後一層或者倒數第二層,而且最後一層的葉子節點在左邊連續,倒數第二層的葉子節點在右邊連續,我們稱爲完全二叉樹。
 *
 * 對排序分爲三步:
 * 1、將無序序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆;
 * 2、將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
 * 3、重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。
 * 說明:
 * 第一步直接將二叉樹對應的數組 調整成 大頂堆或者 小頂堆
 * 然後對第二步、第三步進行循環操作即可。
 * */
public class T4_HeapSort {

    public static void main(String[] args) {
        // 默認升序排序
//        int[] array = {4, 6, 8, 5, 9};
        int[] array = {4, 6, 8, 5, 9, -1, 90, 89, 56, -999};

        System.out.println("原始數組:");
        System.out.println(Arrays.toString(array));

        heapSort(array);

        System.out.println("測試堆排序速度:");
        testTime();  // 8000數據:88ms; 8萬數據: 122ms; 80萬數據:380ms; 800萬數據:4s
    }

    /*
     * 測試一下 堆排序的速度, 給 80000 個數據,測試一下
     * */
    public static void testTime() {
        // 創建一個 80000個的隨機的數組
        int array2[] = new int[8000000];
        for (int i = 0; i < 8000000; i++) {
            array2[i] = (int) (Math.random() * 8000000); // 生成一個[ 0, 8000000] 數
        }
//        System.out.println(Arrays.toString(array2)); // 不在打印,耗費時間太長


        long start = System.currentTimeMillis();  //返回以毫秒爲單位的當前時間
        System.out.println("long start:" + start);
        Date date = new Date(start); // 上面的也可以不要,但是我想測試
        System.out.println("date:" + date);
        SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
        System.out.println("排序前的時間是=" + format.format(date));

        heapSort(array2);

        System.out.println();
        long end = System.currentTimeMillis();
        Date date2 = new Date(end); // 上面的也可以不要,但是我想測試
        System.out.println("排序後的時間是=" + format.format(date2));
        System.out.println("共耗時" + (end - start) + "毫秒");
        System.out.println("毫秒轉成秒爲:" + ((end - start) / 1000) + "秒");
    }


    /*
     * 編寫一個堆排序的方法
     * 核心:將樹排成 大頂堆或者小頂堆。
     * 1、將一個數組(對應二叉樹), 調整成一個大頂堆
     * 2、將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
     * 3、重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。
     * 循環 2、3 步
     * */
    public static void heapSort(int[] array) {
        int temp = 0;
        //分步調整
//        System.out.println("調整成大頂堆:");
//        adjustHeap(array, 1, array.length);
//        System.out.println("第 1 次:"+Arrays.toString(array));  // [4, 9, 8, 5, 6]
//
//        adjustHeap(array, 0, array.length);
//        System.out.println("第 2 次:"+Arrays.toString(array));  // [9, 6, 8, 5, 4]

        /*
         * 完成我們最終代碼 , 對上面的 兩步規律 進行整合,使用for 循環,使用 array.length / 2 - 1 找到第一個非葉子結點。
         * i = array.length / 2 - 1 : 從左到右,從下到上的第一個非葉子節點的 索引
         * */
        for (int i = array.length / 2 - 1; i >= 0; i--) {
            adjustHeap(array, i, array.length);
        }
//        System.out.println("調整成的大頂堆:"+Arrays.toString(array));  // [9, 6, 8, 5, 4]

        /*
         * 2).將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
         * 3).重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。
         * */
        for (int j = array.length - 1; j > 0; j--) {
            // 交換: 將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
            temp = array[j];
            array[j] = array[0];
            array[0] = temp;
            // 每互換一次,都要對根結點 進行調整爲大頂堆。
            adjustHeap(array, 0, j);
        }
//        System.out.println("排序後:"+Arrays.toString(array));  // [9, 6, 8, 5, 4]
    }




    /*
     * 將一個數組(對應二叉樹), 調整成一個大頂堆
     * 功能: 完成將 以 i 對應的非葉子結點的樹,調整成大頂堆
     * 舉例: int[] array = {4, 6, 8, 5, 9}; => i = 1 => adjustHeap => {4, 9, 8, 5, 6}
     * 再次調用adjustHeap 傳入的是   i = 0 => {9, 4, 8, 5, 6}
     * 再次調用adjustHeap 傳入的是   i = 0 => {9, 4, 8, 5, 6} 進行調整 => {9, 6, 8, 5, 4}
     *
     * @param array 待調整的數組
     * @param i 表示 非葉子節點 的在數組中的索引,就是當前結點
     * @param length 表示對多少個元素進行調整,length是在逐漸減少
     * */
    public static void adjustHeap(int[] array, int i, int length) {

        int temp = array[i]; // 先取出 當前 i結點 的值,保存在臨時變量
        /*
         * 開始調整
         * 1、k = i * 2 + 1: k 是以 i 爲非葉子結點的 左子結點
         * */
        for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
            if (k + 1 < length && array[k] < array[k + 1]) { // 說明左子結點  小於 右子結點的值
                k++; // 讓 k 指向 右子結點,這時  k 爲最大值的索引
            }
            if (array[k] > temp) { // 如果右(左)子結點 大於 父結點,說明這裏要對右(左)子結點(k)和父結點(i)進行 互換
                array[i] = array[k]; // 把較大的值賦給當前結點
                array[k] = temp;
                i = k; // !!! i指向 k ,改變父結點 ,繼續循環比較
            } else {
                break; //!!! 敢break 是因爲 這裏的i 是從左到右,從下到上,第一個的非葉子節點
            }
        }

        /*
         * 老師是寫在這兒,我寫在了上面的判斷中 :array[k] = temp;
         * 當代碼走到這兒,for循環結束後,已經將以 i 爲父結點的樹的最大值,放在了最頂(局部)
         * */
//        array[i] = temp;// 將 temp 值 放到調整後的位置。
    }
}

4.2 測試結果

在這裏插入圖片描述

800萬數據 僅用 4S ,可見速度之快。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章