天天看點

資料結構與算法之排序算法資料結構與算法之排序算法

資料結構與算法之排序算法

排序算法的介紹

​ 排序也稱排序算法(Sort Algorithm),排序是将一組資料,依指定的順序進行排序的過程。

排序的分類

  • 1)内部排序:指将需要處理的資料都加載到内部存儲器(記憶體) 中進行排序
  • 2)外部排序:資料量過大,無法全部加載到記憶體中,需要借助外部存儲(檔案等) 進行排序
  • 3)常見的排序算法分類
資料結構與算法之排序算法資料結構與算法之排序算法

算法的時間複雜度

度量一個程式(算法)執行時間的兩種方法

  • 1)事後統計的方法:

    ​ 這種方法可行,但是有兩個問題,一是要想對設計的算法的運作性能進行評測,需要實際運作該程式;二是所得時間的統計量依賴于計算機的硬體、軟體等環境因素,這種方式,要在同一台計算機的相同狀态下運作,才能比較哪個算法速度更快。

  • 2)事前估算的方法

    ​ 通過分析某個算法的時間複雜度來判斷哪個算法更優

時間頻度

介紹

一個算法花費的時間與算法中語句的執行次數成正比例,哪個算法中語句執行次數多,它花費的時間就多。一個算法中的語句執行次數成為語句頻度或時間頻度 。記為T(n)

案例1:計算1-100所有數字的和
int total = 0;
int end = 100;
//使用for循環計算
for(int i = 1;i <= end; i++){
    total += i;
}
// T(n) = n+1;
//直接計算
total = (1+end)*end/2;
// T(n) = 1;
           
案例2:忽略常數項
資料結構與算法之排序算法資料結構與算法之排序算法

結論:

  • 1)2n+20和2n 随着n變大,執行曲線無限接近,20可以忽略
  • 2)3n+10和3n 随着n變大,執行曲線無限接近,10可以忽略
案例3:忽略低次項
資料結構與算法之排序算法資料結構與算法之排序算法

結論:

  • 1)2n^2+3n+10 和2n^2 随着n變大,執行曲線無限接近,3n+10可以忽略
  • 2)n^2+5n+20 和n^2 随着n變大,執行曲線無限接近,5n+20可以忽略
案例4:忽略系數
資料結構與算法之排序算法資料結構與算法之排序算法

結論:

  • 1)随着n值變大,5n^ 2+7n和3n^2+2n,執行曲線重合,說明這種情況下,5和3可以忽略
  • 2)而n^ 3+5n和6n^3+4n,執行曲線分離,說明多少次方關是鍵

時間複雜度

  • 一般情況下,算法中的基本操作語句 重複執行次數是問題規模n的某個函數,用T(n)表示,若有某個輔助函數f(n),使得當n趨近于無窮大時,T(n)/f(n)的極限值為不等于零的常數,則稱f(n)是T(n)的同數量級函數。記做T(n)=O(f(n)),稱O(f(n))為算法的漸進時間複雜度,簡稱時間複雜度。
  • T(n)不同,但時間複雜度可能相同。如:T(n)=n^ 2+7n+6與T(n)=3n^ 2+2n+2,他們的T(n)不同,但時間複雜度相同,都為O(n^2)
  • 計算時間複雜度的方法:
    • 用常數1代替運作時間中的所有加法常數 :T(n)=n^ 2+7n+6=>T(n)=n^2+7n+1
    • 修改後的運作此數函數中,隻保留最高階項 :T(n)=n^ 2+7n+1=>T(n)=n^2
    • 去除最高階項的系數 :T(n)=n^ 2=>T(n)=n^ 2 =>O(n)=n^2

常見的時間複雜度

  • 1)常數階 O(1)

無論代碼執行了多少行,隻要沒有循環等複雜結構,那這個代碼的時間複雜度就都是O(1)

int i=1;
int j=1;
++i;
j++;
int m = i + j;
           

上述代碼在執行的時候,它消耗的時間并不随着某個變量的增長而增長,那麼無論這類代碼有多長,即使有幾萬幾十萬行,都可以用O()

  • 2)對數階O(nlog2n)
int i=1;
while(i<n){
    i = i * 2;
   
}
           

說明: 在while循環裡面,每次都将i乘以2,乘完之後,i距離n就越來越近了。假設循環x次之後,i就大于2了,此時這個循環就退出了,也就是說2的x次方等于n,那麼x=log2n也就是說循環log2n次以後,這個代碼就結束了。是以這個代碼的時間複雜度為:O(log2n)。O(log2n)的這個2時間上是根據代碼變化的,i=i*3,則是O()

  • 3)線性階O(n)
for(int i = 0;i < n;i++){
    j = i;
    j++;
}
           

說明: 這段代碼,for循環裡面的代碼會執行n遍,是以它消耗的時間是随着n的變化而變化的,是以這類代碼都可以用O()

  • 4)線性對數階O(nlog2n)
for(int i = 0;i < n;i++){
    m = 1;
    while(m < n){
        m = m * 2;
    }
}
           

說明: 線性對數階其實非常容易了解,将時間複雜度為O(logn)的代碼循環N遍的話,那麼它的時間複雜度就是n*O(logN),也就是O(nlogN)

  • 5)平方階O(n^2)(雙層for循環)
for(int i = 0;i<=n;i++){
    for(int j = i;j<=n;j++){
        m = i * j;
    }
}
           

說明: 平方階就更容易了解了,如果把O(n)的代碼再嵌套循環一遍,它的時間複雜度就是O(n^ 2),這段代碼其實就是嵌套了2層n循環,它的時間複雜度就是O(nn),即O(n^2),如果将其中一層循環的n改成m,那麼它的時間複雜度就變成了O(mn)

  • 6)立方階O(n^3)(三層for循環)
  • 7)k次方階O(n^k)(k層for循環)

說明: 參考上面的O(n^2)去了解就好了,O()

  • 8)指數階O(2^n)(應盡量避免指數階算法)

常見的時間複雜度對應的圖

資料結構與算法之排序算法資料結構與算法之排序算法

說明:

  • 常見的算法時間複雜度由小到大依次為:O(1)<O(log2n)<O(n)<O(nlog2n)<O(n^ 2)<O(n^ 3)<O(n^k)<O(n!),随着問題規模n的不斷增大,上述時間複雜度不斷增大,算法的執行效率越低
  • 從圖中可見,我們應該盡量避免使用指數階的算法

平均時間複雜度和最壞時間複雜度

  • 1)平均時間複雜度是指所有可能的輸入執行個體均以等機率出現的情況下,該算法的運作時間
  • 2)最壞情況下的時間複雜度稱最壞時間複雜度。一般讨論的時間複雜度均是最壞情況下的時間複雜度。 這樣做的原因是:最壞情況下的時間複雜度是算法在任何輸入執行個體上運作時間的界限,這就保證了算法的運作時間不會比最壞情況更長
  • 3)平均時間複雜度和最壞時間複雜度是否一緻,和算法有關(如圖):
    資料結構與算法之排序算法資料結構與算法之排序算法

算法的空間複雜度

  • 1)類似時間複雜度的讨論,一個算法的空間複雜度(Space Complexity)定義為該算法所耗費的存儲空間,它也是問題規模n的函數。
  • 2)空間複雜度是對一個算法在運作過程中臨時占用存儲空間大小的量度。有的算法需要占用的臨時工作單元數與解決問題的規模n有關,它随着n的增大而增大,當n較大時,将占用較多的存儲單元,例如快速排序和歸并排序算法,基數排序 就屬于這種情況。
  • 3)在做算法分析時,主要讨論的是時間複雜度。從使用者使用體驗上看,更看重的是程式執行的速度。 一些緩存産品(redis,memcache)和算法(基數排序)本質就是用空間換時間。

常用的排序算法

冒泡排序

基本介紹

冒牌排序(Bubble Sorting)的基本思想是:通過對待排序序列從前向後(從下标較小的元素開始),依次比較相鄰元素的值,若發現逆序則交換, 使值較大的元素逐漸從前移向後部,就像水底下的氣泡一樣逐漸向上冒。

優化:

因為排序的過程中,各元素不斷接近自己的位置,如果一趟比較下來沒有進行過交換,就說明序列有序。 是以要在排序過程中設定一個标志flag判斷元素是否進行過交換。進而減少不必要的比較。(這裡的優化,可以在冒泡排序寫好後,在進行)

圖解冒泡排序

資料結構與算法之排序算法資料結構與算法之排序算法

小結:

  • 1)一共進行 數組的大小-1 次大的循環
  • 2)每一堂排序的次數在逐漸的減少
  • 3)如果我們發現在某趟排序中,沒有發生一次交換,可以提前結束冒泡排序,這就是優化。

案例

有五個無序的數:3,9,-2,10,-2,使用冒牌排序法将其排成一個從小到大的有序數列

package cn.aixuxi.sort;

import java.util.Arrays;

public class BubbleSort {
    public static void main(String[] args) {
        int arr[] = {3, 9, -1, 10, -2};
        //測試冒泡排序
        System.out.println("排序前:");
        System.out.println(Arrays.toString(arr));
        bubble(arr);
        System.out.println("排序後:");
        System.out.println(Arrays.toString(arr));
        //為了容易了解,我們把冒泡排序的演變過程,展示出來
        //第一趟排序,将最大的數排在最後
//        int temp = 0;//臨時變量
//        for (int i = 0; i < arr.length - 1; i++) {
            //如果前面的數比後面的數大,則交換
//            if (arr[i] > arr[i + 1]) {
//                temp = arr[i];
//                arr[i] = arr[i + 1];
//                arr[i + 1] = temp;
//            }
//        }
//        System.out.println("第一趟排序後的數組");
//        System.out.println(Arrays.toString(arr));
        //第二趟排序,就是将第二大的數字排在倒數第二位
//        for (int i = 0; i < arr.length - 1 - 1; i++) {
            //如果前面的數比後面的數大,則交換
//            if (arr[i] > arr[i + 1]) {
//                temp = arr[i];
//                arr[i] = arr[i + 1];
//                arr[i + 1] = temp;
//            }
//        }
//        System.out.println("第二趟排序後的數組");
//        System.out.println(Arrays.toString(arr));
        //第三趟排序,就是将第三大的數字排在倒數第三位
//        for (int i = 0; i < arr.length - 1 - 2; i++) {
            //如果前面的數比後面的數大,則交換
//            if (arr[i] > arr[i + 1]) {
//                temp = arr[i];
//                arr[i] = arr[i + 1];
//                arr[i + 1] = temp;
//            }
//        }
//        System.out.println("第三趟排序後的數組");
//        System.out.println(Arrays.toString(arr));
        //第四趟排序,就是将第四大的數字排在倒數第四位
//        for (int i = 0; i < arr.length - 1 - 3; i++) {
//            如果前面的數比後面的數大,則交換
//            if (arr[i] > arr[i + 1]) {
//                temp = arr[i];
//                arr[i] = arr[i + 1];
//                arr[i + 1] = temp;
//            }
//        }
//        System.out.println("第四趟排序後的數組");
//        System.out.println(Arrays.toString(arr));
    }

    //封裝的優化冒泡排序算法
    public static void bubble(int[] arr) {
        int temp = 0;
        //冒泡排序 時間複雜度為O(n^2)
        //優化
        boolean flag = true;//辨別變量,表示是否進行過交換
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - 1; j++) {
                //如果前面的數比後面的大,則交換
                if (arr[j] > arr[j + 1]) {
                    flag = true;
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
            if (!flag) {
                //在一趟排序中,一次交換都沒有發生過
                break;
            } else {
                flag = false;//重置flag,進行下次的判斷
            }
        }
    }
}
           

選擇排序

基本介紹

​ 選擇式排序也屬于内部排序法,是從欲排序的資料中,按指定的規則選出某一進制素,再以規定交換位置後達到排序的目的

選擇排序思想

​ 選擇排序(select sorting)也是一種簡單的排序方法。它的基本思想是:第一次從arr[0]~ arr[n-1]中選取最小值,與arr[0]交換,第二次從arr[1]~ arr[n-1]中選取最小值,與arr[1]交換,第三次從arr[2] ~ arr[n-1]中選取最小值,與arr[2]交換,……,第i次從arr[i-1] ~ arr[n-1]中選取最小值,與arr[i-1]交換……,第n-1次從arr[n-2]~arr[n-1]中選取最小值,與arr[n-2]交換,總共通過n-1次,得到一個排序碼從小到大排列的有序序列。

思路分析圖

資料結構與算法之排序算法資料結構與算法之排序算法

說明:

  • 1)選擇排序一共有數組大小-1 輪排序
  • 2)每一輪排序,又是一個循環,循環的規則(代碼)
  • 3)先假定目前這個數是最小數
  • 4)然後和後面的每個數進行比較,如果發現有比目前數更小的數,就重新确定最小數,并得到下标
  • 5)當周遊到數組的最後時,就得到每輪最小數和下标
  • 6)交換代碼中在繼續循環

案例

使用選擇排序從低到高進行排序:101 34 119 1

package cn.aixuxi.sort;

import java.util.Arrays;

public class SelectSort {
    public static void main(String[] args) {
        int[] arr = {101,34,119,1};
        selectSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    //算法:先簡單 ==> 做複雜,就是可以把一個複雜的算法,拆分成簡單的問題,并逐漸解決
    public static void selectSort(int[] arr){
        // 根據以下推導,簡化代碼
        for (int i = 0; i < arr.length; i++) {
            int minIndex = i;
            int min = arr[i];
            for (int j = i+1; j < arr.length; j++) {
                if (min>arr[j]){
                    min = arr[j];
                    minIndex = j;
                }
            }
            if (minIndex!=i){
                arr[minIndex] = arr[i];
                arr[i] = min;
            }
        }
        //使用逐漸的方式,講解選擇排序
        //第一輪
        //原始的數組:101,34,119,1
        //第一輪排序:1,34,119,101
//        int minIndex = 0;
//        int min = arr[minIndex];
//        for (int i = 1; i < arr.length ; i++) {
//            if (min>arr[i]){//說明假定的最小值,并不是最小值
//                min = arr[i];//重置min
//                minIndex = i;//重置minIndex
//            }
//        }
        //将最小值,放在arr[0],即交換
//        if (minIndex!=0){
//            arr[minIndex] = arr[0];
//            arr[0] = min;
//        }
        //第二輪
//        minIndex = 1;
//        min = arr[minIndex];
//        for (int i = 2; i < arr.length ; i++) {
//            if (min>arr[i]){//說明假定的最小值,并不是最小值
//                min = arr[i];//重置min
//                minIndex = i;//重置minIndex
//            }
//        }
        //将最小值,放在arr[0],即交換
//        if (minIndex!=1){
//            arr[minIndex] = arr[1];
//            arr[1] = min;
//        }
        //第三輪
//        minIndex = 2;
//        min = arr[minIndex];
//        for (int i = 3; i < arr.length ; i++) {
//            if (min>arr[i]){//說明假定的最小值,并不是最小值
//                min = arr[i];//重置min
//                minIndex = i;//重置minIndex
//            }
//        }
        //将最小值,放在arr[2],即交換
//        if (minIndex!=2){
//            arr[minIndex] = arr[2];
//            arr[2] = min;
//        }
    }
}
           

插入排序

基本介紹

插入式排序屬于内部排序法,是對于欲排序的元素以插入的方式找尋該元素的适當位置,以達到排序的目的

插入排序法思想

插入排序(Insertion Sorting)的基本思想:把n個待排序的元素看成為一個有序表和一個無序表,開始時有序表隻包含一個元素,無序表包含有n-1個元素,排序過程中,每次從無序表中取出第一個元素,把它的排序碼依次有序與有序表的排序碼進行比較,将它插入到有序表的适當位置,使之成為新的有序表。

插入排序思路圖

資料結構與算法之排序算法資料結構與算法之排序算法

案例

請用插入排序法将101,34,119,1從小到大進行排序

package cn.aixuxi.sort;

import java.util.Arrays;

public class InsertSort {
    public static void main(String[] args) {
        int[] arr = {101,34,119,1};
        insertSort(arr);
        System.out.println(Arrays.toString(arr));

    }
    //插入排序
    public static void insertSort(int[] arr){
        //根據以下推導,簡化代碼
        for (int i = 1; i < arr.length; i++) {
            int insertVal = arr[i];
            int insertIndex = i - 1;
            while (insertIndex >= 0 && insertVal<arr[insertIndex]){
                arr[insertIndex+1] = arr[insertIndex];
                insertIndex--;
            }
            arr[insertIndex+1] = insertVal;
        }
        //逐漸推導,便于了解
        //第一輪 {101,34,119,1} => {34,101,119,1}
        //定義待插入的數
//        int insertVal = arr[1];
//        int insertIndex = 0;//即arr[1]前面這個數的下标
        //給insertVal找到插入的位置
        /**
         * 說明:
         * 1.insertIndex >= 0 保證在給insertVal找插入位置,不越界
         * 2.insertVal < arr[insertIndex] 待插入的數,還沒有找到插入位置
         * 3.就需要将arr[insertIndex]後移
         */
//        while (insertIndex >= 0 && insertVal<arr[insertIndex]){
//            arr[insertIndex+1] = arr[insertIndex];//
//            insertIndex--;
//        }
        //當退出while循環時,說明插入的位置找到,insertIndex+1;
//        arr[insertIndex+1] = insertVal;

        //第二輪
//        insertVal = arr[2];
//        insertIndex = 1;
//        while (insertIndex >= 0 && insertVal<arr[insertIndex]){
//            arr[insertIndex+1] = arr[insertIndex];//
//            insertIndex--;
//        }
//        arr[insertIndex+1] = insertVal;
        //第三輪
//        insertVal = arr[3];
//        insertIndex = 2;
//        while (insertIndex >= 0 && insertVal<arr[insertIndex]){
//            arr[insertIndex+1] = arr[insertIndex];//
//            insertIndex--;
//        }
//        arr[insertIndex+2] = insertVal;
    }
}
           

希爾排序

簡單插入排序存在的問題

假設數組 arr = {2,3,4,5,6,1},這時需要插入的數1(最小),這樣的過程是:

{2,3,4,5,6,6}
{2,3,4,5,5,6}
{2,3,4,4,5,6}
{2,3,3,4,5,6}
{2,2,3,4,5,6}
{1,2,3,4,5,6}
           

結論:當需要插入的數是較小的數時,後移的次數明顯增多,對效率有影響。

希爾排序算法介紹

希爾排序是希爾(Donald Shell)于1959年提出的一種排序算法。希爾排序也是一種插入排序,它是簡單插入排序經過改進之後的一個更高校的版本,也成為縮小增量排序。

希爾排序算法基本思想

希爾排序是把記錄按下标的一定增量分組,對魅族使用直接插入排序法排序;随着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個檔案恰被分成一組,算法遍終止。

希爾排序的示意圖

資料結構與算法之排序算法資料結構與算法之排序算法
資料結構與算法之排序算法資料結構與算法之排序算法

案例

請對數組arr={8,9,1,7,2,3,5,4,6,0}從小到大進行排序,要求:

1)希爾排序時,對有序序列在插入時采用交換法

2)希爾排序時,對有序序列在插入時采用移動法

package cn.aixuxi.sort;

import java.util.Arrays;

public class ShellSort {
    public static void main(String[] args) {
        int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
        //換位法
        shellSort(arr);
        System.out.println(Arrays.toString(arr));
        //移位法
        shellSort2(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void shellSort(int[] arr) {
        int temp = 0;
        //根據逐漸推導,使用循環處理(交換法)
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < arr.length; i++) {
                //周遊各組中所有元素(共gap組)步長gap
                for (int j = i - gap; j >= 0; j -= gap) {
                    //如果目前元素大于加上步長後的元素,則交換
                    if (arr[j] > arr[j + gap]) {
                        temp = arr[j];
                        arr[j] = arr[j + gap];
                        arr[j + gap] = temp;
                    }
                }

            }
        }
        //逐漸推導的方式編寫希爾排序(交換法)
        //希爾排序的第一輪
        //第一輪排序,将10個資料分成了5組
//        for (int i = 5; i < arr.length; i++) {
//            //周遊個組中的所有元素(共5組,每組有2個元素),步長5
//            for (int j = i - 5; j >= 0; j -= 5) {
//                //如果目前元素大于加上步長後的那個元素,說明交換
//                if (arr[j] > arr[j + 5]) {
//                    temp = arr[j];
//                    arr[j] = arr[j + 5];
//                    arr[j + 5] = temp;
//                }
//            }
//        }
        //希爾排序第二輪
        //第二輪排序,将10個資料分成了5/2組
//        for (int i = 2; i < arr.length; i++) {
//            for (int j = i - 2; j >= 0; j -= 2) {
//                //如果目前元素大于加上步長後的那個元素,說明交換
//                if (arr[j] > arr[j + 2]) {
//                    temp = arr[j];
//                    arr[j] = arr[j + 2];
//                    arr[j + 2] = temp;
//                }
//            }
//        }
        //希爾排序第三輪
        //第三輪排序,将10個資料分成了2/2組
//        for (int i = 1; i < arr.length; i++) {
//            for (int j = i - 1; j >= 0; j -= 1) {
//                //如果目前元素大于加上步長後的那個元素,說明交換
//                if (arr[j] > arr[j + 1]) {
//                    temp = arr[j];
//                    arr[j] = arr[j + 1];
//                    arr[j + 1] = temp;
//                }
//            }
//        }

    }
    //對交換式的希爾排序的優化==>移位法
    public static void shellSort2(int[] arr){
        //增量gap,并逐漸的縮小增量
       for (int gap = arr.length / 2; gap > 0; gap /= 2) {
           //從第gap個元素,逐個對其所在的組進行直接插入排序
           for (int i = gap; i < arr.length; i++) {
               int j = i;
               int temp = arr[i];
               if (arr[j]<arr[j-gap]){
                   while (j-gap>=0 && temp < arr[j-gap]){
                        //移動
                       arr[j] = arr[j-gap];
                       j-=gap;
                   }
                   //當退出while循環後,就給temp找到了插入的位置
                   arr[j] = temp;
               }
           }
       }
    }
}
           

快速排序

快速排序法介紹

快速排序(Quicksort)是對冒泡排序的一種改進。基本思想是:通過一趟排序,将要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分資料都要小,然後再按此方法對着兩部分資料分别進行快速排序,整個排序過程可以遞歸進行,以此達到整個資料變成有序序列

快速排序法示意圖

資料結構與算法之排序算法資料結構與算法之排序算法
資料結構與算法之排序算法資料結構與算法之排序算法

案例

對arr={-9,78,0,23,-567,70}進行從小到大的排序,要求使用快速排序法

package cn.aixuxi.sort;

import java.util.Arrays;

public class QuickSort {
    public static void main(String[] args) {
        int[] arr = {-9, 78, 0, 23, -567, 70};
        quickSort(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }

    public static void quickSort(int[] arr, int left, int right) {
        int l = left;//左下标
        int r = right;//右下标
        //pivot 中軸值
        int pivot = arr[(left + right) / 2];
        int temp = 0;//臨時變量,用于交換時使用
        /**
         * while循環的目的是讓比pivot值小的放到左邊
         * 比pivot值大的放到右邊
         */
        while (l < r) {
            //在pivot 的左邊一直找,找到大于等于pivot的值退出
            while (arr[l] < pivot) {
                l += 1;
            }
            //在pivot 的右邊一直找,找到小于等于pivot的值退出
            while (arr[r] > pivot) {
                r -= 1;
            }
            //如果l>=r成立,說明pivot的左右兩邊的值,已經按照左邊全部是
            //小于等于pivot的值,右邊全部是大于等于pivot的值
            if (l >= r) {
                break;
            }
            //交換
            temp = arr[l];
            arr[l] = arr[r];
            arr[r] = temp;

            //判斷交換完後發現arr[l] == pivot值相等,r--,前移
            if (arr[l] == pivot) {
                r -= 1;
            }
            //判斷交換完後發現arr[r] == pivot值相等,l++,後移
            if (arr[r] == pivot) {
                l += 1;
            }
        }
        //如果l==r,必須l++,r--,否則會出現棧溢出
        if (l == r) {
            l += 1;
            r -= 1;
        }
        //向左遞歸
        if (left < r) {
            quickSort(arr, left, r);
        }
        //向右遞歸
        if (right > l) {
            quickSort(arr, l, right);
        }
    }
}
           

歸并排序

歸并排序介紹

歸并排序(MERGE-SORT)是利用歸并的思想實作的排序方法,該算法采用經典的分治(divide-and-conquer)政策(分治問題将問題分(divide)成一些小的問題然後遞歸求解,而治(conquer)的階段将分的階段得到的各答案“修補”在一起,即分而治之)

歸并排序思想示意圖

基本思想
資料結構與算法之排序算法資料結構與算法之排序算法
合并相鄰有序子序列

在治階段,我們需要将兩個已經有序的子序列合并成一個有序序列,比如上圖中的最後一次合并,要将[4,5,7,8]和[1,2,3,6]兩個已經有序的子序列,合并為最終序列[1,2,3,4,5,6,7,8],步驟如下:

資料結構與算法之排序算法資料結構與算法之排序算法

案例

對數組arr={8,4,5,7,1,3,6,2}進行從小到大的排序,要求:使用歸并排序

package cn.aixuxi.sort;

import java.util.Arrays;

public class MergetSort {
    public static void main(String[] args) {
        int[] arr = {8, 4, 5, 6, 1, 3, 6, 2};
        int[] temp = new int[arr.length];//歸并排序需要額外的空間
        mergetSort(arr,0,arr.length-1,temp);
        System.out.println(Arrays.toString(arr));
    }
    //分+合的方法
    public static void mergetSort(int[] arr,int left,int right,int[] temp) {
        if (left<right){
            int mid = (left+right)/2;//中間的索引
            //向左遞歸進行分解
            mergetSort(arr,left,mid,temp);
            //向右遞歸進行分解
            mergetSort(arr,mid+1,right,temp);
            //到合并
            merge(arr,left,mid,right,temp);
        }
    }
    /**
     * 合并的方法
     *
     * @param arr   排序的原始數組
     * @param left  左邊有序序列的初始索引
     * @param mid   中間索引
     * @param right 右邊索引
     * @param temp  做中轉的數組
     */
    public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        int i = left;//初始化i,左邊有序序列的初始索引
        int j = mid + 1;//初始化j,右邊有序序列的初始索引
        int t = 0;// 指向temp數組的目前索引

        //(一)
        //先把左右兩邊(有序)的資料按照規則填充到temp數組
        //直到左右兩邊的有序序列,有一邊處理完畢為止
        while (i <= mid && j <= right) {//繼續
            if (arr[i] <= arr[j]) {
                //如果左邊的有序序列的目前元素,小于等于右邊的有序序列的元素
                //就把左邊的目前元素拷貝到temp數組
                //然後t++,i++
                temp[t] = arr[i];
                t += 1;
                i += 1;
            } else {//反之,将右邊有序序列的目前元素,填充到temp數組
                temp[t] = arr[j];
                t += 1;
                j += 1;
            }
        }
        //(二)
        //把有剩餘資料的一邊的資料依次全部填充到temp
        while (i <= mid){
            //說明左邊的有序序列還有剩餘的元素,就全部填充到temp中
            temp[t] = arr[i];
            t++;
            i++;
        }
        while (j <= right){
            //說明右邊的有序序列還有剩餘的元素,就全部填充到temp中
            temp[t] = arr[j];
            t++;
            j++;
        }
        //(三)
        //将temp數組的元素拷貝到arr
        //注意,并不是每次都拷貝所有的
        t = 0;
        int tempLeft= left;//
        while (tempLeft<=right){
            //目前數組:
            //第一次合并,tempLeft = 0;right = 1
            //第二次合并,tempLeft = 1;right = 3
            //第三次合并,tempLeft = 0;right = 3
            //……
            //最後一次合并,tempLeft = 0;right = 7
            arr[tempLeft] = temp[t];
            t++;
            tempLeft++;
        }
    }
}
           

基數排序

基數排序(桶排序)介紹

  • 1)基數排序(radix sort)屬于“配置設定式排序”(distribution sort),又稱“桶子法”(bucket sort)或bin sort,顧名思義,它是通過鍵值的各個位的值,将要排序的元素配置設定到某些“桶”中,達到排序的作用
  • 2)基數排序法是屬于穩定性的排序,基數排序法是效率高的穩定性排序法
  • 3)基數排序是桶排序的擴充
  • 4)基數排序是1887年郝爾曼`何樂禮發明的。它是這樣實作的:将整數位按位數切割成不同的數字,然後按每個位數分别比較

基數排序思想

将所有待比較數值統一為同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列

基數排序示意圖

将數組{53,3,542,748,14,214}使用基數排序,進行升序排序

資料結構與算法之排序算法資料結構與算法之排序算法
資料結構與算法之排序算法資料結構與算法之排序算法

案例

将數組{53,3,542,748,14,214}使用基數排序,進行升序排序

package cn.aixuxi.sort;

import java.util.Arrays;

public class RadixSort {
    public static void main(String[] args) {
        int[] arr = {53, 3, 542, 748, 14, 214};
        radixSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    //基數排序
    public static void radixSort(int[] arr) {
        //定義一個二維數組,表示10個桶,每個桶就是一個一維數組
        //說明
        //1.二維數組包含10個一維數組
        //2.為了防止在放入數的時候,資料溢出,則每個一維數組(桶),大小為arr.length
        //3.明顯,基數排序是明顯的空間換時間的算法
        int[][] bucket = new int[10][arr.length];
        //為了記錄每個桶中,實際存放了多少個資料,我們定義一個一維數組來定義各個桶每次放入的資料個數
        int[] bucketElementCounts = new int[10];
        int index;
        //根據推導過程,我們可以得到基數排序的
        //1.得到數組最大的數的位數
        int max = arr[0];//假設第一個數就是最大數
        for (int i = 0; i < arr.length; i++) {
            if (max < arr[i]) {
                max = arr[i];
            }
        }
        //2.得到最大數是幾位數
        int maxLength = (max + "").length();

        for (int k = 0, n = 1; k < maxLength; k++, n *= 10) {
            //針對每個元素的對應位進行排序處理,第一次個位,第二次十位等
            for (int i = 0; i < arr.length; i++) {
                //取出每個元素對應位的值
                int digitOfElement = arr[i] / n % 10;
                //放入到對應的桶中
                bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[i];
                bucketElementCounts[digitOfElement]++;
            }
            //按照這個桶的順序(一維數組的下标依次取出資料,放入原來數組中)
            index = 0;
            //周遊每一桶,并将桶中的資料放入原數組
            for (int i = 0; i < bucketElementCounts.length; i++) {
                //如果桶中有資料,我們才放入到原數組
                if (bucketElementCounts[i] != 0) {
                    //循環該桶,即第i個桶(即第i個一維數組),放入資料
                    for (int j = 0; j < bucketElementCounts[i]; j++) {
                        //取出元素,放入到arr中
                        arr[index++] = bucket[i][j];
                    }
                }
                //每一輪處理後,需要将每個bucketElementCounts[i] = 0
                bucketElementCounts[i] = 0;
            }
        }




        //第一輪排序(針對每個元素的個位進行排序處理
//        for (int i = 0; i < arr.length; i++) {
            //取出每個元素的個位的值
//            int digitOfElement = arr[i] % 10;
            //放入到對應的桶中
//            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[i];
//            bucketElementCounts[digitOfElement]++;
//        }
        //按照這個桶的順序(一維數組的下标依次取出資料,放入原來數組中)
//        int index = 0;
        //周遊每一桶,并将桶中的資料放入原數組
//        for (int i = 0; i < bucketElementCounts.length; i++) {
            //如果桶中有資料,我們才放入到原數組
//            if (bucketElementCounts[i] != 0) {
                //循環該桶,即第i個桶(即第i個一維數組),放入資料
//                for (int j = 0; j < bucketElementCounts[i]; j++) {
                    //取出元素,放入到arr中
//                    arr[index++] = bucket[i][j];
//                }
//            }
            //第一輪處理後,需要将每個bucketElementCounts[i] = 0
//            bucketElementCounts[i] = 0;
//        }

        //第二輪排序(針對每個元素的十位進行排序處理
//        for (int i = 0; i < arr.length; i++) {
            //取出每個元素的個位的值
//            int digitOfElement = arr[i] / 10 % 10;
            //放入到對應的桶中
//            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[i];
//            bucketElementCounts[digitOfElement]++;
//        }
        //按照這個桶的順序(一維數組的下标依次取出資料,放入原來數組中)
//        index = 0;
        //周遊每一桶,并将桶中的資料放入原數組
//        for (int i = 0; i < bucketElementCounts.length; i++) {
            //如果桶中有資料,我們才放入到原數組
//            if (bucketElementCounts[i] != 0) {
                //循環該桶,即第i個桶(即第i個一維數組),放入資料
//                for (int j = 0; j < bucketElementCounts[i]; j++) {
                    //取出元素,放入到arr中
//                    arr[index++] = bucket[i][j];
//                }
//            }
//            bucketElementCounts[i] = 0;
//        }

        //第三輪排序(針對每個元素的百位進行排序處理
//        for (int i = 0; i < arr.length; i++) {
            //取出每個元素的個位的值
//            int digitOfElement = arr[i] / 100 % 10;
            //放入到對應的桶中
//            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[i];
//            bucketElementCounts[digitOfElement]++;
        }
        //按照這個桶的順序(一維數組的下标依次取出資料,放入原來數組中)
//        index = 0;
        //周遊每一桶,并将桶中的資料放入原數組
//        for (int i = 0; i < bucketElementCounts.length; i++) {
            //如果桶中有資料,我們才放入到原數組
//            if (bucketElementCounts[i] != 0) {
                //循環該桶,即第i個桶(即第i個一維數組),放入資料
//                for (int j = 0; j < bucketElementCounts[i]; j++) {
                    //取出元素,放入到arr中
//  i                  arr[index++] = bucket[i][j];
//                }
//            }
//        }
    }
}
           

基數排序的說明

  • 1)基數排序是對傳統桶排序的擴充,速度很快
  • 2)基數排序是經典的空間換時間的方式,占用記憶體很大,當對海量資料排序時,容易造成OutOfMemoryError
  • 基數排序是穩定的。【注:假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱為不穩定的】
  • 有負數的數組,我們不用基數排序來進行排序,如果要支援負數,參考https://code.i-harness.com/zh-CN/q/e98fa9

基數排序

總結和對比

一張圖對比排序算法

資料結構與算法之排序算法資料結構與算法之排序算法

相關術語

  • 1)穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面
  • 2)不穩定:如果a原本在b前面,而a=b,排序之後a可能會出現在b的後面
  • 3)内排序:所有排序操作都在記憶體中完成
  • 4)外排序:由于資料太大,是以把資料放在磁盤中,而排序通過磁盤和記憶體的資料傳輸才能進行
  • 5)時間複雜度:一個算法執行所耗費的時間
  • 6)空間複雜度:運作完一個程式所需記憶體的大小
  • 7)n:資料規模
  • 8)k:“桶”的個數
  • 9)In-place:不占用額外記憶體
  • 10)Out-place:占用額外記憶體

今日小結

其實排序算法中還有一個堆排序,不過和我接下來要繼續複習的二叉樹内容相關。這些算法以前也學過,但是現在重新看一下,還是受益良多,感覺感觸更深。溫故而知新,古人誠不欺我。

繼續閱讀