常見的比較排序


一、冒泡排序(Bubble Sort)

【原理】

  比較兩個相鄰的元素,將值大的元素交換至右端。

【思路】

  依次比較相鄰的兩個數,將小數放在前面,大數放在后面。即在第一趟:首先比較第1個和第2個數,將小數放前,大數放后。然后比較第2個數和第3個數,將小數放前,大數放后,如此繼續,直至比較最后兩個數,將小數放前,大數放后。重復第一趟步驟,直至全部排序完成。

  第一趟比較完成后,最后一個數一定是數組中最大的一個數,所以第二趟比較的時候最后一個數不參與比較;

  第二趟比較完成后,倒數第二個數也一定是數組中第二大的數,所以第三趟比較的時候最后兩個數不參與比較;

  依次類推,每一趟比較次數-1;

  ……

【舉例】——要排序數組:int[] arr={6,3,8,2,9,1};   

  第一趟排序:

    第一次排序:6和3比較,6大於3,交換位置:  3  6  8  2  9  1

    第二次排序:6和8比較,6小於8,不交換位置:3  6  8  2  9  1

    第三次排序:8和2比較,8大於2,交換位置:  3  6  2  8  9  1

    第四次排序:8和9比較,8小於9,不交換位置:3  6  2  8  9  1

    第五次排序:9和1比較:9大於1,交換位置:  3  6  2  8  1  9

    第一趟總共進行了5次比較, 排序結果:      3  6  2  8  1  9

  ---------------------------------------------------------------------

  第二趟排序:

    第一次排序:3和6比較,3小於6,不交換位置:3  6  2  8  1  9

    第二次排序:6和2比較,6大於2,交換位置:  3  2  6  8  1  9

    第三次排序:6和8比較,6大於8,不交換位置:3  2  6  8  1  9

    第四次排序:8和1比較,8大於1,交換位置:  3  2  6  1  8  9

    第二趟總共進行了4次比較, 排序結果:      3  2  6  1  8  9

  ---------------------------------------------------------------------

  第三趟排序:

    第一次排序:3和2比較,3大於2,交換位置:  2  3  6  1  8  9

    第二次排序:3和6比較,3小於6,不交換位置:2  3  6  1  8  9

    第三次排序:6和1比較,6大於1,交換位置:  2  3  1  6  8  9

    第二趟總共進行了3次比較, 排序結果:         2  3  1  6  8  9

  ---------------------------------------------------------------------

  第四趟排序:

    第一次排序:2和3比較,2小於3,不交換位置:2  3  1  6  8  9

    第二次排序:3和1比較,3大於1,交換位置:  2  1  3  6  8  9

    第二趟總共進行了2次比較, 排序結果:        2  1  3  6  8  9

  ---------------------------------------------------------------------

  第五趟排序:

    第一次排序:2和1比較,2大於1,交換位置:  1  2  3  6  8  9

    第二趟總共進行了1次比較, 排序結果:  1  2  3  6  8  9

  ---------------------------------------------------------------------

  最終結果:1  2  3  6  8  9

  ---------------------------------------------------------------------

  由此可見:N個數字要排序完成,總共進行N-1趟排序,每i趟的排序次數為(N-i)次,所以可以用雙重循環語句,外層控制循環多少趟,內層控制每一趟的循環次數,即

for(int i=1;i<arr.length;i++){

    for(int j=1;j<arr.length-i;j++){

    //交換位置

}

  冒泡排序的優點:每進行一趟排序,就會少比較一次,因為每進行一趟排序都會找出一個較大值。如上例:第一趟比較之后,排在最后的一個數一定是最大的一個數,第二趟排序的時候,只需要比較除了最后一個數以外的其他的數,同樣也能找出一個最大的數排在參與第二趟比較的數后面,第三趟比較的時候,只需要比較除了最后兩個數以外的其他的數,以此類推……也就是說,每進行一趟比較,每一趟少比較一次,一定程度上減少了算法的量。

  用時間復雜度來說:

  1.如果我們的數據正序,只需要走一趟即可完成排序。所需的比較次數C和記錄移動次數M均達到最小值,即:Cmin=n-1;Mmin=0;所以,冒泡排序最好的時間復雜度為O(n)。

  2.如果很不幸我們的數據是反序的,則需要進行n-1趟排序。每趟排序要進行n-i次比較(1≤i≤n-1),且每次比較都必須移動記錄三次來達到交換記錄位置。在這種情況下,比較和移動次數均達到最大值:

    

  冒泡排序的最壞時間復雜度為:O(n2) 。

  綜上所述:冒泡排序總的平均時間復雜度為:O(n2) 。

【代碼實現】

public class BubbleSort {
    public static void main(String[] args) {
        int[] arr = {6, 3, 8, 2, 9, 1};
        System.out.println("排序前數組:");
        for (int num : arr) {
            System.out.println(num + " ");
        }

        for (int i = 0; i < arr.length - 1; i++) {//外層循環控制排序趟數
            for (int j = 0; j < arr.length - 1 - i; j++) {//內層循環控制每一趟排序多少次
                if (arr[j] > arr[j + 1]) {
                    swap(arr, j, j + 1);
                }
            }
        }

        System.out.println("------------");
        System.out.println("排序后數組:");
        for (int num : arr) {
            System.out.println(num + " ");
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

二、選擇排序(SelectionSort)

【原理】

   每一趟從待排序的記錄中選出最小的元素,順序放在已排好序的序列最后,直到全部記錄排序完畢。也就是:每一趟在n-i+1(i=1,2,…n-1)個記錄中選取關鍵字最小的記錄作為有序序列中第i個記錄。

【基本思想】(簡單選擇排序)

  給定數組:int[] arr={里面n個數據};第1趟排序,在待排序數據arr[1]~arr[n]中選出最小的數據,將它與arrr[1]交換;第2趟,在待排序數據arr[2]~arr[n]中選出最小的數據,將它與arr[2]交換;以此類推,第i趟在待排序數據arr[i]~arr[n]中選出最小的數據,將它與arr[i]交換,直到全部排序完成。

【舉例】——數組 int[] arr={5,2,8,4,9,1}; 

  第一趟排序:

  最小數據1,把1放在首位,也就是1和5互換位置,

  排序結果:1  2  8  4  9  5

  -------------------------------------------------------

  第二趟排序:

  第1以外的數據{2  8  4  9  5}進行比較,2最小,

  排序結果:1  2  8  4  9  5

  -------------------------------------------------------

  第三趟排序:

  除1、2以外的數據{8  4  9  5}進行比較,4最小,8和4交換

  排序結果:1  2  4  8  9  5

  -------------------------------------------------------

  第四趟排序:

  除第1、2、4以外的其他數據{8  9  5}進行比較,5最小,8和5交換

  排序結果:1  2  4  5  9  8

  -------------------------------------------------------

  第五趟排序:

  除第1、2、4、5以外的其他數據{9  8}進行比較,8最小,8和9交換

  排序結果:1  2  4  5  8  9

  -------------------------------------------------------

  注:每一趟排序獲得最小數的方法:for循環進行比較,定義一個第三個變量temp,首先前兩個數比較,把較小的數放在temp中,然后用temp再去跟剩下的數據比較,如果出現比temp小的數據,就用它代替temp中原有的數據。

【代碼實現】

public class SelectionSort {
    public static void main(String[] args) {
        int[] arr = {5, 2, 8, 4, 9, 1};
        System.out.println("交換之前:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
        // 做第i趟排序
        for (int i = 0; i < arr.length - 1; i++) {
            int minIndex = i;
            // 選最小的記錄
            for (int j = i + 1; j < arr.length; j++) {
                //記下目前找到的最小值所在的位置
                minIndex = arr[j] < arr[minIndex] ? j : minIndex;
            }
            swap(arr, i, minIndex);
        }

        System.out.println();
        System.out.println("交換后:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

  選擇排序的時間復雜度:簡單選擇排序的比較次數與序列的初始排序無關。 假設待排序的序列有n個元素,則比較次數永遠都是n (n - 1) / 2。而移動次數與序列的初始排序有關。當序列正序時,移動次數最少,為 0。當序列反序時,移動次數最多,為3n (n - 1) /  2。

  所以,綜上,簡單排序的時間復雜度為 O(n²)。

三、插入排序(Insertion sort)

  插入排序對於少量元素的排序是很高效的,而且這個排序的手法在每個人生活中也是有的哦。你可能沒有意識到,當你打牌的時候,就是用的插入排序。

【概念】

  從桌上的牌堆摸牌,牌堆內是雜亂無序的,但是我們摸上牌的時候,卻會邊摸邊排序,借用一張算法導論的圖。
  
  每次我們從牌堆摸起一張牌,然后將這張牌插入我們左手捏的手牌里面,在插入手牌之前,我們會自動計算將牌插入什么位置,然后將牌插入到這個計算后的位置,雖然這個計算轉瞬而過,但我們還是嘗試分析一下這個過程:

  1. 我決定摸起牌后,最小的牌放在左邊,摸完后,牌面是從左到右依次增大
  2. 摸起第1張牌,直接捏在手里,現在還不用排序
  3. 摸起第2張牌,查看牌面大小,如果第二張牌比第一張牌大,就放在右邊
  4. 摸起第3張牌,從右至左開始計算,先看右邊的牌,如果摸的牌比最右邊的小,那再從右至左看下一張,如果仍然小,繼續順延,直到找到正確位置(循環)
  5. 摸完所有的牌,結束

  所以我們摸完牌,牌就已經排完序了。講起來有點拗口,但是你在打牌的時候絕對不會覺得這種排序算法會讓你頭疼。這就是傳說中的插入排序。

  想象一下,假如我們認為左手拿的牌和桌面的牌堆就是同一數組,當我們摸完牌以后,我們就完成了對這個數組的排序。

【示例】

  

  上圖就是插入排序的過程,我們把它想象成摸牌的過程。
  格子上方的數字:表示格子的序號,圖(a)中,1號格子內的數字是5,2號格子是2,3號格子是4,以此類推
  灰色格子:我們手上已經摸到的牌
  黑色格子:我們剛剛摸起來的牌
  白色格子:桌面上牌堆的牌

  1、圖(a),我們先摸起來一張5,然后摸起來第二張2,發現25小,於是將5放到2號格子,2放到1號格子(簡單的人話:將2插到5前面)

  2、圖(b),摸起來一張4,比較4和2號格子內的數字545小,於是將5放到3號格子,再比較4和1號格子內的24大於24小於5,於是這就找到了正確的位置。(說人話:就是摸了張4點,將45交換位置)

  3、圖(c)、圖(d)、圖(e)和圖(f),全部依次類推,相信打牌的你能夠看懂。
  看到這里,我相信應該沒人看不懂什么是插入排序了,那么插入排序的代碼長什么模樣:

【代碼實現】

public class InsertionSort {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 7, 5, 2, 3, 3, 1};
        System.out.println("排序前:");
        for (int num : arr) {
            System.out.print(num + " ");
        }

        //插入排序
        for (int i = 1; i < arr.length; i++) {
            for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
                swap(arr, j, j + 1);
            }
        }
        System.out.println();
        System.out.println("排序后:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

【時間復雜度】

  • 最好情況下,數組已經是有序的,每插入一個元素,只需要考查前一個元素,因此最好情況下,插入排序的時間復雜度為O(N)
  • 在最壞情況下,數組完全逆序,插入第2個元素時要考察前1個元素,插入第3個元素時,要考慮前2個元素,……,插入第N個元素,要考慮前 N - 1 個元素。因此,最壞情況下的比較次數是 1 + 2 + 3 + ... + (N - 1),等差數列求和,結果為 N² / 2,所以最壞情況下的復雜度為 O(N²)

   當數據狀況不同,產生的算法流程不同的時候,一律按最差的估計,所以插入排序是O(N²)的算法。

四、歸並排序(MERGE-SORT)

【基本思想】

  歸並排序(MERGE-SORT)是利用歸並的思想實現的排序方法,該算法采用經典的分治(divide-and-conquer)策略(分治法將問題(divide)成一些小的問題然后遞歸求解,而治(conquer)的階段則將分的階段得到的各答案"修補"在一起,即分而治之)。

  可以看到這種結構很像一棵完全二叉樹,本文的歸並排序我們采用遞歸去實現(也可采用迭代的方式去實現)。階段可以理解為就是遞歸拆分子序列的過程,遞歸深度為log2n。

【合並相鄰有序子序列】

再來看看階段,我們需要將兩個已經有序的子序列合並成一個有序序列,比如上圖中的最后一次合並,要將[4,5,7,8]和[1,2,3,6]兩個已經有序的子序列,合並為最終序列[1,2,3,4,5,6,7,8],來看下實現步驟。

【代碼實現】

public class MergeSort {
    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        sortProcess(arr, 0, arr.length - 1);
    }

    public static void sortProcess(int[] arr, int L, int R) {
        if (L == R) {
            return;
        }
        //L和R中點的位置,相當於(L+R)/2
        int mid = L + ((R - L) >> 1);
        //左邊歸並排序,使得左子序列有序
        sortProcess(arr, L, mid);
        //右邊歸並排序,使得右子序列有序
        sortProcess(arr, mid + 1, R);
        //將兩個有序子數組合並操作
        merge(arr, L, mid, R);
    }

    public static void merge(int[] arr, int L, int mid, int R) {
        int[] temp = new int[R - L + 1];
        int i = 0;
        //左序列指針
        int p1 = L;
        //右序列指針
        int p2 = mid + 1;
        while (p1 <= mid && p2 <= R) {
            temp[i++] = arr[p1] < arr[p1] ? arr[p1++] : arr[p2++];
        }
        //兩個必有且只有一個越界,即以下兩個while只會發生一個
        //p1沒越界,潛台詞是p2必越界
        while (p1 <= mid) {
            //將左邊剩余元素填充進temp中
            temp[i++] = arr[p1++];
        }
        while (p2 <= R) {
            //將右序列剩余元素填充進temp中
            temp[i++] = arr[p2++];
        }

        //將輔助數組temp中的的元素全部拷貝到原數組中
        for (int j = 0; j < temp.length; j++) {
            arr[L + j] = temp[j];
        }
    }

    public static void main(String[] args) {
        int[] arr = {9, 8, 7, 6, 5, 4, 3, 2, 1};
        mergeSort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

【時間復雜度】

  根據歸並排序的流程,可以看出整個流程的時間復雜度的表達式為:T(N)=2T(N/2)+O(N),所以歸並排序的時間復雜度為O(N*logN)

五、快速排序

  快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然后再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
首先來了解一下經典快排:

5.1 經典快速排序

  其中就小於等於的區域可以優化一下,小於的放小於區域,等於的放等於區域,大於的放大於區域。這就演變成荷蘭國旗問題了。

【荷蘭國旗問題】

  給定一個數組arr,和一個數num,請把小於num的數放在數組的左邊,等於num的數放在數組的中間,大於num的數放在數組的 右邊。

  大致過程如圖:

  

  當前數小於num時,該數與小於區域的下一個數交換,小於區域+1;當前數等於num時,繼續比較下一個;當前數大於num時,該數與大於區域的前一個數交換,指針不變,繼續比較當前位置。

  代碼如下:

public class NetherlandsFlag {

    public static int[] partition(int[] arr, int L, int R, int num) {
        int less = L - 1;
        int more = R + 1;
        int cur = L;
        while (cur < more) {
            if (arr[cur] < num) {
                //當前數小於num時,當前數和小於區域的下一個數交換,然后小於區域擴1位置,cur往下跳
                swap(arr, ++less, cur++);
            } else if (arr[cur] > num) {
                //當前數大於num時,大於區域的前一個位置的數和當前的數交換,且當前數不變,繼續比較
                swap(arr, --more, cur);
            } else {
                //當前數等於num時,直接下一個比較
                cur++;
            }
        }
        //返回等於區域的范圍
        //less+1是等於區域的第一個數
        //more-1是等於區域的最后一個數
        return new int[]{less + 1, more - 1};
    }

    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    // for test
    public static int[] generateArray() {
        int[] arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int) (Math.random() * 3);
        }
        return arr;
    }

    // for test
    public static void printArray(int[] arr) {
        if (arr == null) {
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        int[] test = generateArray();
        printArray(test);
        int[] res = partition(test, 0, test.length - 1, 1);
        printArray(test);
        System.out.println(res[0]);
        System.out.println(res[1]);

    }
}

5.2 隨機快速排序(優化版)

 【基本思想】

  從一個數組中隨機選出一個數N,通過一趟排序將數組分割成三個部分:小於N的區域;等於N的區域 ;大於N的區域,然后再按照此方法對小於區的和大於區分別遞歸進行,從而達到整個數據變成有序數組。

 【圖解流程】

  下面通過實例數組進行排序,存在以下數組

  從上面的數組中,隨機選取一個數(假設這里選的數是5)與最右邊的7進行交換 ,如下圖

  准備一個小於區和大於區(大於區包含最右側的一個數)等於區要等最后排完數才會出現,並准備一個指針,指向最左側的數,如下圖

  到這里,我們要開始排序了,每次操作我們都需要拿指針位置的數與我們選出來的數進行比較,比較的話就會出現三種情況,小於,等於,大於。三種情況分別遵循下面的交換原則:

  1. 指針的數<選出來的數
    1.1 拿指針位置的數與小於區右邊第一個數進行交換
    1.2 小於區向右擴大一位
    1.3 指針向右移動一位
  2. 選出來的數=選出來的數
    2.1 指針向右移動一位
  3. 指針的數>選出來的數
    3.1 拿指針位置的數與大於區左邊第一個數進行交換
    3.2 大於區向左擴大一位
    3.3 指針位置不動 

  根據上面的圖可以看出5=5,滿足交換原則第2點,指針向右移動一位,如下圖

  

   從上圖可知,此時3<5,根據交換原則第1點,拿3和5(小於區右邊第一個數)交換,小於區向右擴大一位,指針向右移動一位,結果如下圖

  

  從上圖可以看出,此時7>5,滿足交換原則第3點,7和2(大於區左邊第一個數)交換,大於區向左擴大一位,指針不動,如下圖

  

  從上圖可以看出,2<5,滿足交換原則第1點,2和5(小於區右邊第一個數)交換,小於區向右擴大一位,指針向右移動一位,得到如下結果

  

  從上圖可以看出,6>5,滿足交換原則第3點 ,6和6自己換,大於區向左擴大一位,指針位置不動,得到下面結果

  

  此時,指針與大於區相遇,則將指針位置的數6與隨機選出來的5進行交換,就可以得到三個區域:小於區,等於區,大於區,如下: 

  

   到此,一趟排序結束了,后面再將小於區和大於區重復剛剛的流程即可得到有序的數組。

【代碼實現】

public class QuickSort {
    public static void quickSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        quickSort(arr, 0, arr.length - 1);
    }

    public static void quickSort(int[] arr, int L, int R) {
        if (L < R) {
            //隨機產生一個數和最右邊的數交換
            swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
            int[] p = partition(arr, L, R);
            //p[0] - 1表示等於區域的左邊界
            quickSort(arr, L, p[0] - 1);
            //p[1] + 1表示等於區域的右邊界
            quickSort(arr, p[1] + 1, R);
        }
    }

    public static int[] partition(int[] arr, int L, int R) {
        int less = L - 1;
        int more = R;
        while (L < more) {
            if (arr[L] < arr[R]) {
                swap(arr, ++less, L++);
            } else if (arr[L] > arr[R]) {
                swap(arr, --more, L);
            } else {
                L++;
            }
        }
        swap(arr, more, R);
        return new int[]{less + 1, more};
    }

    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    // for test
    public static void comparator(int[] arr) {
        Arrays.sort(arr);
    }

    // for test
    public static int[] generateRandomArray(int maxSize, int maxValue) {
        int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
        }
        return arr;
    }

    // for test
    public static int[] copyArray(int[] arr) {
        if (arr == null) {
            return null;
        }
        int[] res = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            res[i] = arr[i];
        }
        return res;
    }

    // for test
    public static boolean isEqual(int[] arr1, int[] arr2) {
        if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
            return false;
        }
        if (arr1 == null && arr2 == null) {
            return true;
        }
        if (arr1.length != arr2.length) {
            return false;
        }
        for (int i = 0; i < arr1.length; i++) {
            if (arr1[i] != arr2[i]) {
                return false;
            }
        }
        return true;
    }

    // for test
    public static void printArray(int[] arr) {
        if (arr == null) {
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        int testTime = 50000;
        int maxSize = 10;
        int maxValue = 100;
        boolean succeed = true;
        for (int i = 0; i < testTime; i++) {
            int[] arr1 = generateRandomArray(maxSize, maxValue);
            int[] arr2 = copyArray(arr1);
            quickSort(arr1);
            comparator(arr2);
            if (!isEqual(arr1, arr2)) {
                succeed = false;
                printArray(arr1);
                printArray(arr2);

            }
        }
        System.out.println(succeed ? "Nice!" : "error~~");

        int[] arr = generateRandomArray(maxSize, maxValue);
        printArray(arr);
        quickSort(arr);
        printArray(arr);
    }
}

【時間復雜度】

  快排的時間復雜度O(N*logN)空間復雜度O(logN) 【因為每次都是隨機事件,壞的情況和差的情況,是等概率的,根據數學期望值可以算出時間復雜度和空間復雜度】,不穩定性排序

六、堆排序

  堆排序是利用這種數據結構而設計的一種排序算法,堆排序是一種選擇排序 ,要知道堆排序的原理我們首先一定要知道什么是堆。 

6.1 什么是堆

  這里,必須引入一個完全二叉樹的概念,然后過渡到堆的概念。

  

  上圖,就是一個完全二叉樹,其特點在於:

1.葉子節點只可能在層次最大的兩層出現;
2.對於最大層次中的葉子節點,都依次排列在該層的最左邊的位置上;
3.如果有度為1的葉子節點,只可能有1個,且該節點只有左孩子而沒有右孩子。

  那么,完全二叉樹與堆有什么關系呢?

  我們假設有一棵完全二叉樹,在滿足作為完全二叉樹的基礎上,每個結點的值都大於或等於其左右孩子結點的值,稱為大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱為小頂堆。如下圖:

  同時,我們對堆中的結點按層進行編號,將這種邏輯結構映射到數組中就是下面這個樣子

  

  該數組從邏輯上講就是一個堆結構,我們用簡單的公式來描述一下堆的定義就是:

  大頂堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]  

  小頂堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]  

  ok,了解了這些定義。接下來,我們來看看堆排序的基本思想及基本步驟。

6.2 堆排序基本思想及步驟

【基本思想】

  將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就為最大值。然后將剩余n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反復執行,便能得到一個有序序列了。  

【步驟】

  第一步:構造初始堆。將給定無序序列構造成一個大頂堆(一般升序采用大頂堆,降序采用小頂堆)。

  假設給定無序序列結構如下

  此時我們從最后一個非葉子結點開始(葉結點自然不用調整,第一個非葉子結點 arr.length/2-1=5/2-1=1,也就是下面的6結點),從左至右,從下至上進行調整。

  找到第二個非葉節點4,由於[4,9,8]中9元素最大,4和9交換。

  這時,交換導致了子根[4,5,6]結構混亂,繼續調整,[4,5,6]中6最大,交換4和6。

 

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

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

   將堆頂元素9和末尾元素4進行交換

  重新調整結構,使其繼續滿足堆定義

  再將堆頂元素8與末尾元素5進行交換,得到第二大元素8.

  后續過程,繼續進行調整,交換,如此反復進行,最終使得整個序列有序

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

  a.將無需序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆;

  b.將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;

  c.重新調整結構,使其滿足堆定義,然后繼續交換堆頂元素與當前末尾元素,反復執行調整+交換步驟,直到整個序列有序。

6.3 代碼實現

/**
 * 堆排序代碼實現
 * @author yi
 */
public class HeapSort {
    public static void heapSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        //建立大頂堆
        for (int i = 0; i < arr.length; i++) {
            heapInsert(arr, i);
        }
        int heapSize = arr.length;
        //最后一個位置的數和0位置的數交換
        swap(arr, 0, --heapSize);
        while (heapSize > 0) {
            //從0位置開始,將當前形成的堆調成大頂堆
            heapify(arr, 0, heapSize);
            swap(arr, 0, --heapSize);
        }
    }

    /**
     * 建立大頂堆,時間復雜度為O(N)
     * @param arr
     * @param index
     */
    public static void heapInsert(int[] arr, int index) {
        //如果當前數比父節點大
        while (arr[index] > arr[(index - 1) / 2]) {
            //和父節點交換
            swap(arr, index, (index - 1) / 2);
            //index往上跑
            index = (index - 1) / 2;
        }
    }

    /**
     * 調整大頂堆(僅是調整過程,建立在大頂堆已構建的基礎上)
     * 一個值變小,往下"沉"的操作
     *
     * @param arr
     * @param index
     * @param heapSize 堆的大小
     */
    public static void heapify(int[] arr, int index, int heapSize) {
        //左孩子
        int left = index * 2 + 1;
        //左孩子在堆上是存在的,沒越界
        while (left < heapSize) {
            //left+1:右孩子
            //右孩子沒越界,且右孩子的值比左孩子大時,那么較大的數就是右孩子的值所在的位置;反之...
            //largest表示左右孩子誰的值更大,誰的下標就是largest
            int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;

            //找到左右孩子兩者的較大值后,再拿這個值和當前數比較,哪個大哪個就作為largest的下標
            largest = arr[largest] > arr[index] ? largest : index;
            if (largest == index) {
                //如果你和你的孩子之間的最大值是你自己,不用再往下"沉"了
                break;
            }
            //當前數和左右孩子之間較大的數交換
            swap(arr, largest, index);
            index = largest;
            left = index * 2 + 1;
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    public static void main(String[] args) {
        int[] arr = {3, 5, 2, 1, 6, 7, 3, 9};
        System.out.println(Arrays.toString(arr));
        heapSort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

 【時間復雜度】

  • 初始化堆的過程:O(n)
  • 調整堆的過程:O(nlogn)

  綜上所述:堆排序的時間復雜度為:O(nlogn)

七、排序算法的穩定性及其意義

7.1 穩定性的定義

  假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱為不穩定的。

7.2 穩定性的意義

  1. 如果只是簡單的進行數字的排序,那么穩定性將毫無意義。
  2. 如果排序的內容僅僅是一個復雜對象的某一個數字屬性,那么穩定性依舊將毫無意義
  3. 如果要排序的內容是一個復雜對象的多個數字屬性,但是其原本的初始順序毫無意義,那么穩定性依舊將毫無意義。
  4. 除非要排序的內容是一個復雜對象的多個數字屬性,且其原本的初始順序存在意義,那么我們需要在二次排序的基礎上保持原有排序的意義,才需要使用到穩定性的算法,例如要排序的內容是一組原本按照價格高低排序的對象,如今需要按照銷量高低排序,使用穩定性算法,可以使得想同銷量的對象依舊保持着價格高低的排序展現,只有銷量不同的才會重新排序。(當然,如果需求不需要保持初始的排序意義,那么使用穩定性算法依舊將毫無意義)。

7.3 常見的排序算法的穩定性分析

冒泡排序】

  冒泡排序就是把小的元素往前調(或者把大的元素往后調)。注意是相鄰的兩個元素進行比較,而且是否需要交換也發生在這兩個元素之間。所以,如果兩個元素相等,我想你是不會再無聊地把它們倆再交換一下。

  如果兩個相等的元素沒有相鄰,那么即使通過前面的兩兩交換把兩個元素相鄰起來,最終也不會交換它倆的位置,所以相同元素經過排序后順序並沒有改變。所以冒泡排序是一種穩定排序算法。 

【選擇排序】

  選擇排序是給每個位置選擇當前元素最小的,比如給第一個位置選擇最小的,在剩余元素里面給第二個元素選擇第二小的,依次類推,直到第n - 1個元素,第n個元素不用選擇了,因為只剩下它一個最大的元素了。那么,在一趟選擇,如果當前元素比一個元素小,而該小的元素又出現在一個和當前元素相等的元素后面,那么交換后穩定性就被破壞了。比較拗口,舉個例子,序列5 8 5 2 9,我們知道第一遍選擇第1個元素5會和2交換,那么原序列中2個5的相對前后順序就被破壞了。所以選擇排序不是一個穩定的排序算法。

插入排序】

  插入排序是在一個已經有序的小序列的基礎上,一次插入一個元素。當然,剛開始這個有序的小序列只有1個元素,就是第一個元素。比較是從有序序列的末尾開始,也就是想要插入的元素和已經有序的最大者開始比起,如果比它大則直接插入在其后面,否則一直往前找直到找到它該插入的位置。如果碰見一個和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后順序沒有改變,從原無序序列出去的順序就是排好序后的順序。所以插入排序是穩定的。

快速排序】

  在快速排序中,是隨機選擇一個數,然后小於它的放左邊,等於它的放中間,大於它的放右邊。默認快速排序是不穩定的。(其實快速排序可以做到穩定性問題,但是非常難,不需要掌握,可以搜“01 stable sort”)。

歸並排序】

  歸並排序是把序列遞歸地分成短序列,遞歸出口是短序列只有1個元素(認為直接有序)或者2個序列(1次比較和交換),然后把各個有序的短序列合並成一個有序的長序列,不斷合並直到原序列全部排好序。可以發現,在1個或2個元素時,1個元素不會交換,2個元素如果大小相等也沒有人故意交換,這不會破壞穩定性。那么,在短的有序序列合並的過程中,穩定是是否受到破壞?沒有,合並過程中我們可以保證如果兩個當前元素相等時,我們把處在前面的序列的元素保存在結果序列的前面,這樣就保證了穩定性。所以,歸並排序也是穩定的排序算法。

【堆排序】

  我們知道堆的結構是節點i的孩子為2 * i和2 * i + 1節點,大頂堆要求父節點大於等於其2個子節點,小頂堆要求父節點小於等於其2個子節點。在一個長為n 的序列,堆排序的過程是從第n / 2開始和其子節點共3個值選擇最大(大頂堆)或者最小(小頂堆),這3個元素之間的選擇當然不會破壞穩定性。但當為n / 2 - 1, n / 2 - 2, ... 1這些個父節點選擇元素時,就會破壞穩定性。有可能第n / 2個父節點交換把后面一個元素交換過去了,而第n / 2 - 1個父節點把后面一個相同的元素沒 有交換,那么這2個相同的元素之間的穩定性就被破壞了。

  舉個簡單的例子,假如有個數組4,4,4,5,5,在建立大頂堆的時候,第二個4會和第一個5的順序調換,這樣元素4的穩定性就被破壞了。所以,堆排序不是穩定的排序算法。

八、工程中的綜合排序算法

  假如有一個大數組,如果這個數組的長度很長,在工程上綜合排序會先進行一個判斷:數組里面裝的是基礎類型(int、double、char...)還是自己定義的類。如果裝的是基礎類型,會選擇快速排序;如果裝的是自己定義的類型,比如一個student類,里面有分數和班級兩個字段,你可能會按照student中的某一個字段來排序,這時候會給你用歸並排序來排;

  但是如果數組的長度很短時,不管數組里面裝的是什么類型,綜合排序都不會選擇快速排序,也不會選擇歸並排序,而是直接用插入排序。為什么要用插入排序呢?因為插入排序的常數項極低,當數據量小於60時,直接用插入排序,雖然插入排序的時間復雜度是O(n²),但是在樣本量極小的情況下,O(n²)的劣勢表現不出來,反而插入排序的常數項很低,導致在小樣本的情況下,插入排序會非常快。所以在整個數組的長度小於60的情況下,是直接用插入排序的。

  在一個數組中,一開始它的長度可能很大,這時候就有分治行為:左邊部分拿去遞歸,右邊部分拿去遞歸。當你遞歸的部分一旦小於60,直接使用插排,當樣本量大於60、很大的時候,才使用快排或歸並的方式,用遞歸的方式化為子問題。原來快排或歸並的遞歸終止條件是:當只剩1個數(L==R)的時候直接返回這個數。而在綜合排序算法中,遞歸終止的條件就改為:L和R相差不到60,即L>R-60時,終止條件就是里面使用插入排序。

  為什么如果數組裝的是基礎類型時使用快速排序,數組裝的是自己定義的類時使用歸並排序呢?這也取決於排序的穩定性。

  因為基礎類型不需要區分原始順序,比如說一個數組里面全部放的是整型{3,3,1,5,4,3,2},排完序后我們並不需要區分這3個“3”的原始順序是怎么樣的,因為基礎類型,相同值無差異。快速排序是不穩定的。

  而如果是自定義的類,比如一個student類,如果我們需要將student先按照分數排序,再按照班級排序,此時,相同班級的個體是不一樣的,是有差別的,所以要用歸並排序,因為歸並排序是穩定的。

 

 

 

 

參考:https://www.cnblogs.com/shen-hua/p/5422676.html

  https://www.cnblogs.com/asis/p/6798779.html

  https://www.cnblogs.com/chengxiao/p/6194356.html

https://www.cnblogs.com/pipipi/p/9460249.html

https://blog.csdn.net/u010452388/article/details/81218540

https://blog.csdn.net/u013384984/article/details/79496052

https://www.cnblogs.com/chengxiao/p/6129630.html

https://www.cnblogs.com/tigerson/p/7156648.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM