天天看點

徹底搞懂排序算法,看這篇就夠了

文章目錄

  • ​​1.排序算法的介紹和分類​​
  • ​​2.算法的時間複雜度和空間複雜度​​
  • ​​2.1 時間頻度忽略常數、低次項、系數的說明​​
  • ​​2.2 時間複雜度的計算​​
  • ​​2.3 常見的時間複雜度​​
  • ​​2.3.1 常數階O( 1 )​​
  • ​​2.3.2 對數階O( log2n )​​
  • ​​2.3.3 線性階O( n )​​
  • ​​2.3.4 線性對數階O( nlogN )​​
  • ​​2.3.5 平方階O( n² )​​
  • ​​2.3.6 立方階O( n³ )、K次方階O( n^k )​​
  • ​​2.4 算法的平均時間複雜度和最壞時間複雜度​​
  • ​​2.5 空間複雜度​​
  • ​​3.冒泡排序​​
  • ​​3.1 代碼實作​​
  • ​​4.選擇排序​​
  • ​​4.1 代碼實作​​
  • ​​5.插入排序​​
  • ​​5.1 代碼實作​​
  • ​​6.希爾排序​​
  • ​​6.1 代碼實作​​
  • ​​7.快速排序​​
  • ​​7.1 代碼實作​​
  • ​​8.歸并排序​​
  • ​​8.1 代碼實作​​
  • ​​9.基數排序​​
  • ​​9.1 代碼實作​​
  • ​​9.2 基數排序算法的注意事項​​
  • ​​10.堆排序(在二叉樹部分)​​
  • ​​11.常用排序算法的總結和對比​​

1.排序算法的介紹和分類

  • 排序也稱​

    ​排序算法 (Sort Algorithm)​

    ​,排序是将一組資料,依指定的順序進行排列的過程。
  • 排序的分類
  • ​内部排序​

    ​​:指将需要處理的所有資料都加載到​

    ​記憶體中​

    ​進行排序。
  • ​外部排序​

    ​​:資料量過大,無法全部加載到記憶體中,需要借助​

    ​外部存儲​

    ​ (檔案等)進行排序。
  • 常見的排序算法分類
徹底搞懂排序算法,看這篇就夠了

2.算法的時間複雜度和空間複雜度

度量一個程式 (算法) ​

​執行時間​

​的兩種方法

  • 事後統計的方法
  • 這種方法可行,但是有兩個問題:一是要想對設計的算法的運作性能進行評測,需要​

    ​實際運作該程式​

    ​​;二是所得時間的統計量依賴于​

    ​計算機的硬體​

    ​​、​

    ​軟體​

    ​​等環境因素, 這種方式,要在​

    ​同一台計算機​

    ​​的​

    ​相同狀态​

    ​下運作,才能比較那個算法速度更快。
  • 事前估算的方法
  • 通過分析某個算法的​

    ​時間複雜度​

    ​來判斷哪個算法更優
時間頻度
  • 一個算法花費的時間與算法中語句的執行次數成正比例,哪個算法中語句執行次數多,它花費時間就多。​

    ​一個算法中的語句執行次數稱為語句頻度或時間頻度​

    ​,記為T(n)。
時間頻度的舉例說明
public class Sort {

  //比如計算1-100所有數字之和, 設計如下兩種算法:
  public static void main(String[] args) {
    //方法一: 使用for循環計算
    int total = 0;
    int end = 100;
    for(int i = 1;i <= end;i++){
      total += i;
    }
    System.out.println("total = " + total);  //T(n)=n+1;加1是因為最後還要判斷i是不是小于等于end
    
    //方法二: 直接進行計算
    int total2 = 0;
    int end2 = 100;
    total2 = (1 + end2)*end2/2;
    System.out.println("total2 = " + total2); //T(n)=1
  }
  
}      

2.1 時間頻度忽略常數、低次項、系數的說明

  • 舉例說明 -​

    ​忽略常數項​

T(n)=2n+20 T(n)=2*n T(n)=(3n+10) T(n)=(3n)
1 22 2 13 3
2 24 4 16 6
5 30 10 25 15
8 36 16 34 24
15 50 30 55 45
30 80 60 100 90
100 220 200 310 300
300 620 600 910 900
徹底搞懂排序算法,看這篇就夠了
結論:
1. 2n+20 和 2*n 随着 n 變大,執行曲線無限接近, 20可以忽略
2. 3n+10 和 3n 随着 n 變大,執行曲線無限接近, 10可以忽略      
  • 舉例說明 -​

    ​忽略低次項​

T(n)=2*n^2+3n+10 T(n)=(2*n^2) T(n)=(n^2+5n+20) T(n)=(n^2)
1 15 2 26 1
2 24 8 34 4
5 75 50 70 25
8 162 128 124 64
15 505 450 320 225
30 1900 1800 1070 900
100 20310 20000 10520 10000
徹底搞懂排序算法,看這篇就夠了
結論: 
1. 2*n^2+3n+10 和 2*n^2 随着 n 變大, 執行曲線無限接近, 可以忽略 3n+10
2. n^2+5n+20 和 n^2 随着 n 變大, 執行曲線無限接近, 可以忽略 5n+20      
  • 舉例說明 -​

    ​忽略系數​

T(n)=(3*n^2+2n) T(n)=(5*n^2+7n) T(n)=(n^3+5n) T(n)=(6*n^3+4n)
1 5 12 6 10
2 16 34 18 56
5 85 160 150 770
8 208 376 552 3104
15 705 1230 3450 20310
30 2760 4710 27150 162120
100 30200 50700 1000500 6000400
徹底搞懂排序算法,看這篇就夠了
結論: 
1. 随着n值變大,5*n^2+7n 和 3*n^2 + 2n ,執行曲線重合, 說明這種情況下, 5和3可以忽略。
2. 而 n^3+5n 和 6n^3+4n ,執行曲線分離,說明忽略系數不适用于 n^k(k>=3)的情況。      

2.2 時間複雜度的計算

  1. 一般情況下,算法中的基本操作語句的​

    ​重複執行次數​

    ​​是問題規模n的某個函數,用T(n)表示,若有某個輔助函數f(n),使得當n趨近于​

    ​無窮大​

    ​​時,​

    ​T(n) / f(n)​

    ​​的極限值為不等于零的​

    ​常數​

    ​​,則稱​

    ​f(n)是T(n)的同數量級函數​

    ​​。記作​

    ​T(n)=O( f(n) )​

    ​​,稱​

    ​O( f(n) )​

    ​​為算法的漸進時間複雜度,簡稱​

    ​時間複雜度​

    ​。
  2. T(n) 不同,但時間複雜度可能相同。 如:​

    ​T(n)=n²+7n+6​

    ​​ 與​

    ​T(n)=3n²+2n+2​

    ​​ 它們的T(n) 不同,但​

    ​時間複雜度相同​

    ​​,都為​

    ​O(n²)​

    ​。
  3. 計算時間複雜度的方法:
  • 用​

    ​常數1​

    ​​替代運作時間中的所有​

    ​加法常數​

    ​​ T(n)=3n²+7n​

    ​+6​

    ​​ => T(n)=3n²+7n​

    ​+1​

  • 修改後的運作次數函數中,​

    ​隻保留最高階項​

    ​​ T(n)=3n²​

    ​+7n+1​

    ​ => T(n) = 3n²
  • 去除最高階項的系數 T(n) =​

    ​3​

    ​​n² =>​

    ​O(n²)​

2.3 常見的時間複雜度

1.常數階O(1)

2.對數階O(log2n):2是底數,n是真數

3.線性階O(n)

4.線性對數階O(nlog2n)

5.平方階O(n^2)

6.立方階O(n^3)

7.k次方階O(n^k)

8.指數階O(2^n):從圖中可見,我們應該盡量避免使用指數階的算法
  
說明:
常見的算法時間複雜度由小到大依次為:O(1)<O(log2n)<O(n)<O(nlog2n)<O(n^2)<O(n^3)< O(n^k) <O(2^n)      
徹底搞懂排序算法,看這篇就夠了

2.3.1 常數階O( 1 )

  • 無論代碼執行了多少行,隻要是沒有循環等複雜結構,那這個代碼的時間複雜度就都是O(1) 。
public static void main(String[] args) {
        int i = 1;
        int j = 2;
        ++i;
        j++;
        int m = i + j;
    }      
  • 上述代碼在執行的時候,它消耗的時候并不随着某個變量的增長而增長,那麼無論這類代碼有多長,即使有​

    ​幾萬幾十萬行​

    ​,都可以用O(1)來表示它的時間複雜度。

2.3.2 對數階O( log2n )

  • 如果
  • 徹底搞懂排序算法,看這篇就夠了
  • ,即a的x次方等于N(a>0,且a≠1),那麼數x叫做以a為底N的對數(logarithm),記作
  • 徹底搞懂排序算法,看這篇就夠了
  • 。其中,a叫做 對數的​​底數​​,N叫做 ​​真數​​,x叫做​

    ​以a為底N的對數​

    ​。
public static void main(String[] args) {
        int i = 1;
        while (i < n) {
            i = i * 2;
        }
    }      
  • 在while循環裡面,每次都将 i 乘以 2,乘完之後,i 距離 n 就越來越近了。假設循環​

    ​x次​

    ​​之後,i 就​

    ​大于 n​

    ​​ 了,此時這個循環就退出了,也就是說​

    ​2 的 x 次方等于 n​

    ​​,那麼​

    ​x = log2n​

    ​​也就是說當循環​

    ​log2n 次以後​

    ​​,這個代碼就結束了。是以這個代碼的時間複雜度為:​

    ​O(log2n)​

    ​​ 。​

    ​O(log2n)​

    ​​ 的這個2 實際上是根據代碼變化的,​

    ​i = i * 3​

    ​​ ,則是​

    ​O(log3n)​

    ​ 。

2.3.3 線性階O( n )

public static void main(String[] args) {
        int j = 0;
        for (int i = 1;i <= n;i++) {
            j = i;
            j++;
        }
        System.out.println("j = " + j);
  }      
  • 這段代碼,for循環裡面的代碼會​

    ​執行n遍​

    ​​,是以它消耗的時間是​

    ​随着n的變化而變化的​

    ​​,是以這類代碼都可以用​

    ​O(n)​

    ​來表示它的時間複雜度。

2.3.4 線性對數階O( nlogN )

public static void main(String[] args) {
        int i = 0;
        for (int m = 1;m <= n;m++) {
            i = 1;
            while (i < n) {
                i = i * 2;
            }
        }
        System.out.println("i = " + i);
    }      
  • 線性對數階O(nlogN) 其實非常容易了解,将時間複雜度為O(logn)的代碼循環N遍的話,那麼它的時間複雜度就是 n * O(logN),也就是了O(nlogN)。

2.3.5 平方階O( n² )

public static void main(String[] args) {
        int j = 0;
        for (int x = 1;x <= n;x++) {
            for (int i = 1; i < n; i++) {
                j = i;
                j++;
            }
        }
        System.out.println("j = " + j);
    }      
  • 平方階O(n²) 就更容易了解了,如果把 O(n) 的代碼再嵌套循環一遍,它的時間複雜度就是​

    ​O(n²)​

    ​​,這段代碼其實就是嵌套了​

    ​2層n循環​

    ​,它的時間複雜度就是 O(n * n),即 O(n²) 如果将其中一層循環的n改成m,那它的時間複雜度就變成了 O(m * n)。

2.3.6 立方階O( n³ )、K次方階O( n^k )

  • 參考上面的O(n²) 去了解就好了,O(n³)相當于三層n循環,其它的類似。

2.4 算法的平均時間複雜度和最壞時間複雜度

  • ​平均時間複雜度​

    ​​是指所有可能的輸入執行個體均以​

    ​等機率​

    ​出現的情況下,該算法的運作時間。
  • 最壞情況下的時間複雜度稱​

    ​最壞時間複雜度​

    ​​。​

    ​一般讨論的時間複雜度均是最壞情況下的時間複雜度​

    ​。 這樣做的原因是:最壞情況下的時間複雜度是算法在任何輸入執行個體上運作時間的界限,這就保證了算法的運作時間不會比最壞情況更長。
  • 平均時間複雜度和最壞時間複雜度是否一緻,​

    ​和算法有關​

    ​(如下表)。
排序法 平均時間 最差情形 穩定度 額外空間 備注
冒泡 O(n2) O(n2) 穩定 O(1) n小時較好
交換 O(n2) O(n2) 不穩定 O(1) n小時較好
選擇 O(n2) O(n2) 不穩定 O(1) n小時較好
插入 O(n2) O(n2) 穩定 O(1) 大部分已排序時較好
基數 O(logRB) O(logRB) 穩定 O(n) B是真數(0-9),R是基數(個十百)
Shell O(nlogn) O(ns)1<s<2 不穩定 O(1) s是所選分組
快速 O(nlogn) O(n2) 不穩定 O(nlogn) n大時較好
歸并 O(nlogn) O(nlogn) 穩定 O(1) n大時較好
O(nlogn) O(nlogn) 不穩定 O(1) n大時較好

2.5 空間複雜度

  • 類似于時間複雜度的讨論,一個算法的​

    ​空間複雜度(Space Complexity)定義為該算法所耗費的存儲空間​

    ​,它也是問題規模n的函數。
  • 空間複雜度(Space Complexity)是對一個算法在運作過程中臨時占用存儲空間大小的比例。有的算法需要占用的臨時工作單元數與解決問題的規模n有關,它随着n的增大而增大,當n較大時,将占用較多的存儲單元,例如快速排序和歸并排序算法就屬于這種情況。
  • 在做算法分析時,​

    ​主要讨論的是時間複雜度​

    ​。從使用者使用體驗上看,更看重的程式執行的速度。一些緩存産品(redis, memcache)和算法(基數排序)本質就是用空間換時間。

3.冒泡排序

  • 冒泡排序(Bubble Sorting)的基本思想是:通過對​

    ​待排序序列​

    ​從前向後(從下标較小的元素開始),依次比較相鄰元素的值,若發現逆序則交換,使值較大的元素逐漸從前移向後部,就像水底下的氣泡一樣逐漸向上冒。
優化:
因為排序的過程中,各元素不斷接近自己的位置,如果一趟比較下來沒有進行過交換,就說明序列有序,
是以要在排序過程中設定一個标志flag判斷元素是否進行過交換。
進而減少不必要的比較。(這裡說的優化,可以在冒泡排序寫好後,再進行)      

圖解冒泡排序算法的過程

将五個無序的數:​

​3、9、-1、10、20​

​​ 使用​

​冒泡排序​

​​将其排成一個​

​有序數列​

徹底搞懂排序算法,看這篇就夠了
徹底搞懂排序算法,看這篇就夠了
徹底搞懂排序算法,看這篇就夠了
徹底搞懂排序算法,看這篇就夠了
冒泡排序規則:
1. 一共進行(數組的大小-1)次大的循環
2. 每一趟排序的次數在逐漸的減少
3.      
冒泡排序的動态圖
徹底搞懂排序算法,看這篇就夠了

3.1 代碼實作

/**
 * @author xiexu
 * @create 2020-11-04 2:07 下午
 */
public class BubbleSort {
    public static void main(String[] args) {
        int arr[] = {3, 9, -1, 10, -2};

        //第一趟排序,将最大的數排在最後
        int temp = 0; //臨時變量,用于資料交換
        for (int i = 0; i < arr.length - 1 - 0; i++) {
            //如果前面的數比後面的數大,則交換
            if (arr[i] > arr[i + 1]) {
                temp = arr[i];
                arr[i] = arr[i + 1];
                arr[i + 1] = temp;
            }
        }
        System.out.print("第一趟排序後:");
        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.print("第二趟排序後:");
        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.print("第三趟排序後:");
        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.print("第四趟排序後的數組:");
        System.out.println(Arrays.toString(arr));

    }
}      

根據以上的規律,進行優化

public class BubbleSort {
    public static void main(String[] args) {
        int arr[] = {3, 9, -1, 10, -5};
        System.out.print("排序前的數組:");
        System.out.println(Arrays.toString(arr));
        bubbleSort(arr);
    }

    public static void bubbleSort(int[] arr) {
        //冒泡排序的時間複雜度O(n^2)
        int temp = 0; //臨時變量,用于資料交換
        boolean flag = false; //辨別變量,表示是否進行過交換
        for (int j = 0; j < arr.length - 1; j++) {

            for (int i = 0; i < arr.length - 1 - j; i++) {
                //如果前面的數比後面的數大,則交換
                if (arr[i] > arr[i + 1]) {
                    flag = true;
                    temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                }
            }

            System.out.print("第" + (j + 1) + "趟排序後的數組:");
            System.out.println(Arrays.toString(arr));

        }
    }

}      

對代碼進行再次優化

public class BubbleSort {
    public static void main(String[] args) {
        int arr[] = {3, 9, -1, 10, -5};
        System.out.print("排序前的數組:");
        System.out.println(Arrays.toString(arr));
        bubbleSort(arr);
        System.out.print("排序前的數組:");
        System.out.println(Arrays.toString(arr));
    }

    public static void bubbleSort(int[] arr) {
        //冒泡排序的時間複雜度O(n^2)
        int temp = 0; //臨時變量,用于資料交換
        boolean flag = false; //辨別變量,表示是否進行過交換
        for (int j = 0; j < arr.length - 1; j++) {

            for (int i = 0; i < arr.length - 1 - j; i++) {
                //如果前面的數比後面的數大,則交換
                if (arr[i] > arr[i + 1]) {
                    flag = true;
                    temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                }
            }
            if (!flag) { // 在一趟排序中,一次交換都沒有,直接退出
                break;
            }else {
                flag = false; // 重置flag,友善下一次循環判斷
            }
        }
    }

}      

冒泡排序的速度測試

/**
 * @author xiexu
 * @create 2020-11-04 2:07 下午
 */
public class BubbleSort {
    public static void main(String[] args) {
        int arr[] = {3, 9, -1, 10, -5};
        System.out.println("排序前的數組:");
        System.out.println(Arrays.toString(arr));

        //測試一下冒泡排序的速度:O(n^2),給8000個的資料,測試
//      int[] arr = new int[80000];
//      for(int i = 0;i < 80000;i++){
//          arr[i] = (int)(Math.random() * 80000);  //自動生成[0,80000)之間的随機數
//      }
        
        Date date1 = new Date();
        SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String datas = simt.format(date1);
        System.out.println("排序前的時間是:" + datas);

        bubbleSort(arr);
        System.out.println("排序後的數組:");
        System.out.println(Arrays.toString(arr));
        
        Date date2 = new Date();
        SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String datas2 = simt.format(date2);
        System.out.println("排序前的時間是:" + datas2);
    }

    public static void bubbleSort(int[] arr) {
        //冒泡排序的時間複雜度O(n^2)
        int temp = 0; //臨時變量,用于資料交換
        boolean flag = false; //辨別變量,表示是否進行過交換
        for (int j = 0; j < arr.length - 1; j++) {

            for (int i = 0; i < arr.length - 1 - j; i++) {
                //如果前面的數比後面的數大,則交換
                if (arr[i] > arr[i + 1]) {
                    flag = true;
                    temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                }
            }
            if (!flag) { // 在一趟排序中,一次交換都沒有,直接退出
                break;
            }else {
                flag = false; // 重置flag,友善下一次循環判斷
            }

        }
    }

}      
tips:建議了解了冒泡排序算法後,可以自己不看例子手寫一遍,有助于更深的了解!

4.選擇排序

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

選擇排序的思想
選擇排序(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. 每1輪排序,又是一個循環, 循環的規則[見下面代碼]
  2.1 先假定目前這個數是最小數
  2.2 然後和後面的每個數進行比較,如果發現有比目前數更小的數,就重新确定最小數,并得到下标
  2.3 當周遊到數組的最後時,就得到本輪最小數和下标
  2.4 交換 [見下面代碼]      

4.1 代碼實作

/**
 * @author xiexu
 * @create 2020-11-04 5:39 下午
 */
public class SelectSort {

    public static void main(String[] args) {
        int[] arr = {101, 34, 119, 1};
        System.out.print("排序前:");
        System.out.println(Arrays.toString(arr));
        selectSort(arr);
    }

    //選擇排序的時間複雜度O(n^2)
    public static void selectSort(int[] arr) {
        //使用逐漸推導的方式來講解選擇排序
        // 第1輪
        // 原始數組: 101, 34, 119, 1
        // 第一輪排序: 1, 34, 119, 101

        // 第一輪
        int minIndex = 0; //最小值索引
        int min = arr[0]; //先假設第一個數為最小值
        for (int j = 0 + 1; j < arr.length; j++) {
            if (min > arr[j]) {
                min = arr[j]; //重置最小值
                minIndex = j; //重置最小值索引
            }
        }

        //将最小值放在arr[0]的位置,即交換位置
        if (minIndex != 0) {
            arr[minIndex] = arr[0];
            arr[0] = min;
        }

        System.out.print("第一輪後:");
        System.out.println(Arrays.toString(arr));    //第一輪後:[1, 34, 119, 101]

        // 第二輪
        minIndex = 1; //最小值索引
        min = arr[1]; //先假設第二個數為最小值
        for (int j = 1 + 1; j < arr.length; j++) {
            if (min > arr[j]) {
                min = arr[j]; //重置最小值
                minIndex = j; //重置最小值索引
            }
        }

        //将最小值放在arr[1]的位置,即交換位置
        if (minIndex != 1) {
            arr[minIndex] = arr[1];
            arr[1] = min;
        }

        System.out.print("第二輪後:");
        System.out.println(Arrays.toString(arr));    //第二輪後:[1, 34, 119, 101]

        // 第三輪
        minIndex = 2; //最小值索引
        min = arr[2]; //先假設第二個數為最小值
        for (int j = 2 + 1; j < arr.length; j++) {
            if (min > arr[j]) {
                min = arr[j]; //重置最小值
                minIndex = j; //重置最小值索引
            }
        }

        //将最小值放在arr[1]的位置,即交換位置
        if (minIndex != 2) {
            arr[minIndex] = arr[2];
            arr[2] = min;
        }

        System.out.print("第三輪後:");
        System.out.println(Arrays.toString(arr));    //第三輪後:[1, 34, 101, 119]

    }

}      

根據以上的規律,進行優化

/**
 * @author xiexu
 * @create 2020-11-04 5:39 下午
 */
public class SelectSort {

    public static void main(String[] args) {
        int[] arr = {101, 34, 119, 1};
        System.out.print("排序前:");
        System.out.println(Arrays.toString(arr));
        selectSort(arr);
        System.out.print("排序前:");
        System.out.println(Arrays.toString(arr));
    }

    //選擇排序的時間複雜度O(n^2)
    public static void selectSort(int[] arr) {
        //使用逐漸推導的方式來講解選擇排序
        // 第1輪
        // 原始數組: 101, 34, 119, 1
        // 第一輪排序: 1, 34, 119, 101

        for (int i = 0; i < arr.length - 1; 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; //重置最小值索引
                }
            }

            //将最小值放在arr[0]的位置,即交換位置
            if (minIndex != 0) {
                arr[minIndex] = arr[i];
                arr[i] = min;
            }
        }
    }

}      

選擇排序的速度測試

/**
 * @author xiexu
 * @create 2020-11-04 5:39 下午
 */
public class SelectSort {

    public static void main(String[] args) {
        //測試一下冒泡排序的速度:O(n^2),給80000個的資料,測試
        int[] arr = new int[80000];
        for(int i = 0;i < 80000;i++){
            arr[i] = (int)(Math.random() * 80000);    //自動生成[0,80000)之間的随機數
        }

        //排序前的時間:
        Date data = new Date();
        SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS = simt.format(data);
        System.out.println("排序前的時間是:" + dateS);

        selectSort(arr); // 調用選擇排序

        //排序後的時間:
        Date data2 = new Date();
        SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS2 = simt2.format(data2);
        System.out.println("排序後的時間是:" + dateS2);
    }

    //選擇排序
    public static void selectSort(int[] arr) {
        //使用逐漸推導的方式來講解選擇排序
        // 第1輪
        // 原始數組: 101, 34, 119, 1
        // 第一輪排序: 1, 34, 119, 101

        for (int i = 0; i < arr.length - 1; 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; //重置最小值索引
                }
            }

            //将最小值放在arr[0]的位置,即交換位置
            if (minIndex != 0) {
                arr[minIndex] = arr[i];
                arr[i] = min;
            }
        }
    }

}      

5.插入排序

  • 插入式排序屬于​

    ​内部排序法​

    ​,是将一個記錄插入到已經排好序的有序表中,進而得到一個新的、記錄數增1的有序表。
插入排序的思想
插入排序(Insertion Sorting)的基本思想是:
  把n個待排序的元素看成是一個有序表和一個無序表,開始時有序表中隻包含一個元素,無序表中包含有n-1個元素,排序過程中每次從無序表中取出第一個元素,把它的排序碼依次與有序表元素的排序碼進行比較,将它插入到有序表中的适當位置,使之成為新的有序表。      
插入排序的思路分析圖
徹底搞懂排序算法,看這篇就夠了
徹底搞懂排序算法,看這篇就夠了

5.1 代碼實作

/**
 * @author xiexu
 * @create 2020-11-04 8:32 下午
 */
public class InsertSort {

    public static void main(String[] args) {
        int []arr = {101,34,119,1};
        insertSort(arr);
    }

    //插入排序
    public static void insertSort(int[] arr) {
        //使用逐漸推導的方式來講解,偏于了解

        //第1輪{101, 34, 119, 1}; => {34, 101, 119, 1};

        //第一輪
        //定義待插入的數
        int insertVal = arr[1];
        int insertIndex = 1 - 1; //即arr[1]的前面這個數的下标

        //給insertVal 找到插入位置
        //說明:
        //1.insertVal >= 0 保證在找到相應位置時,不會越界
        //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;

        System.out.print("第1輪插入:");
        System.out.println(Arrays.toString(arr));

        //第二輪
        //定義待插入的數
        insertVal = arr[2];
        insertIndex = 2 - 1;  //即arr[2]的前面這個數的下标

        while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
            arr[insertIndex+1] = arr[insertIndex];
            insertIndex--;
        }

        //當退出while循環時,說明找到要插入的位置, insertIndex + 1
        arr[insertIndex + 1] = insertVal;

        System.out.print("第2輪插入:");
        System.out.println(Arrays.toString(arr));

        //第三輪
        //定義待插入的數
        insertVal = arr[3];
        insertIndex = 3 - 1; //即arr[3]的前面這個數的下标

        while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
            arr[insertIndex+1] = arr[insertIndex];
            insertIndex--;
        }

        //當退出while循環時,說明找到要插入的位置, insertIndex + 1
        arr[insertIndex + 1] = insertVal;

        System.out.print("第3輪插入:");
        System.out.println(Arrays.toString(arr));

    }
}      

根據以上規律,進行優化

/**
 * @author xiexu
 * @create 2020-11-04 8:32 下午
 */
public class InsertSort {

    public static void main(String[] args) {
        int []arr = {101,34,119,1,-1,89};
        System.out.print("排序前的數組:");
        System.out.println(Arrays.toString(arr));
        insertSort(arr);
    }

    //插入排序
    public static void insertSort(int[] arr) {
        //使用逐漸推導的方式來講解,偏于了解

        //第1輪{101, 34, 119, 1}; => {34, 101, 119, 1};

        int insertVal = 0; //定義待插入的數
        int insertIndex = 0; //代插入的數的前一個數的下标
        for (int i = 1; i < arr.length; i++) {
            insertVal = arr[i];
            insertIndex = i - 1; //即arr[i]的前面這個數的下标

            //給insertVal 找到插入位置
            //說明:
            //1.insertVal >= 0 保證在找到相應位置時,不會越界
            //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;

            System.out.print("第"+ i +"輪插入:");
            System.out.println(Arrays.toString(arr));
        }

    }
}      

插入排序的速度測試

/**
 * @author xiexu
 * @create 2020-11-04 8:32 下午
 */
public class InsertSort {

    public static void main(String[] args) {
        //測試一下插入排序的速度:O(n^2),給80000個的資料,測試
        int[] arr = new int[80000];
        for(int i = 0;i < 80000;i++){
            arr[i] = (int)(Math.random() * 80000);    //自動生成[0,80000)之間的随機數
        }
      
        //排序前的時間:
        Date data = new Date();
        SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS = simt.format(data);
        System.out.println("排序前的時間是:" + dateS);    //2020-05-29 11:31:53

        insertSort(arr);    //調用插入排序

        //排序後的時間:
        Date data2 = new Date();
        SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS2 = simt2.format(data2);
        System.out.println("排序後的時間是:" + dateS2);    //2020-05-29 11:31:54
    }

    //插入排序
    public static void insertSort(int[] arr) {
        //使用逐漸推導的方式來講解,偏于了解

        //第1輪{101, 34, 119, 1}; => {34, 101, 119, 1};

        int insertVal = 0; //定義待插入的數
        int insertIndex = 0; //代插入的數的前一個數的下标
        for (int i = 1; i < arr.length; i++) {
            insertVal = arr[i];
            insertIndex = i - 1; //即arr[i]的前面這個數的下标

            //給insertVal 找到插入位置
            //說明:
            //1.insertVal >= 0 保證在找到相應位置時,不會越界
            //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;
        }

    }
}      

可以對代碼進行小小的優化,但​

​貌似沒什麼差別​

/**
 * @author xiexu
 * @create 2020-11-04 8:32 下午
 */
public class InsertSort {

    public static void main(String[] args) {
        //測試一下插入排序的速度:O(n^2),給80000個的資料,測試
        int[] arr = new int[80000];
        for(int i = 0;i < 80000;i++){
            arr[i] = (int)(Math.random() * 80000);    //自動生成[0,80000)之間的随機數
        }
        //排序前的時間:
        Date data = new Date();
        SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS = simt.format(data);
        System.out.println("排序前的時間是:" + dateS);    //2020-05-29 11:31:53

        insertSort(arr);    //調用插入排序

        //排序後的時間:
        Date data2 = new Date();
        SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS2 = simt2.format(data2);
        System.out.println("排序後的時間是:" + dateS2);    //2020-05-29 11:31:54
    }

    //插入排序
    public static void insertSort(int[] arr) {
        //使用逐漸推導的方式來講解,偏于了解

        //第1輪{101, 34, 119, 1}; => {34, 101, 119, 1};

        int insertVal = 0; //定義待插入的數
        int insertIndex = 0; //代插入的數的前一個數的下标
        for (int i = 1; i < arr.length; i++) {
            insertVal = arr[i];
            insertIndex = i - 1; //即arr[i]的前面這個數的下标

            //給insertVal 找到插入位置
            //說明:
            //1.insertVal >= 0 保證在找到相應位置時,不會越界
            //2.insertVal < arr[insertIndex] 待插入的數未找到合适位置
            //3.就需要将 arr[insertIndex] 後移
            while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
                arr[insertIndex+1] = arr[insertIndex];
                insertIndex--;
            }
            //當退出while循環時,說明找到要插入的位置, insertIndex + 1
            //這裡我們判斷是否需要指派
            if (insertIndex + 1 == i) {
                arr[insertIndex + 1] = insertVal;
            }
        }

    }
}      

6.希爾排序

針對于上述​

​簡單插入排序​

​,它存在一些問題:
我們看簡單的插入排序可能存在的問題:
  當需要插入的數是較小的數時,後移的次數明顯增多,對效率會有影響。
數組 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時,整個檔案恰被分成一組,算法便終止。      
希爾排序的思路分析圖
徹底搞懂排序算法,看這篇就夠了
徹底搞懂排序算法,看這篇就夠了
希爾排序動态圖
徹底搞懂排序算法,看這篇就夠了
希爾排序時, 對有序序列在插入時采用​

​交換法​

​, 并測試排序速度

希爾排序時, 對有序序列在插入時采用​

​移動法​

​, 并測試排序速度

6.1 代碼實作

/**
 * @author xiexu
 * @create 2020-11-05 11:17 上午
 */
public class ShellSort {

    public static void main(String[] args) {
        int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
        shellSort(arr);
    }

    //使用逐漸推導的方式來編寫希爾排序
    public static void shellSort(int[] arr) {
        int temp = 0;
        // 第1輪排序: 将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;
                }
            }
        }

        System.out.println("第1輪排序:" + Arrays.toString(arr));

        // 第2輪排序: 将10個資料分為 5/2 = 2組
        for (int i = 2; i < arr.length; i++) {
            //周遊各組中的所有元素(共5組,每組有2個元素),步長為5
            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;
                }
            }
        }

        System.out.println("第2輪排序:" + Arrays.toString(arr));

        // 第3輪排序: 将10個資料分為 2/2 組
        for (int i = 1; i < arr.length; i++) {
            //周遊各組中的所有元素(共5組,每組有2個元素),步長為5
            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;
                }
            }
        }

        System.out.println("第3輪排序:" + Arrays.toString(arr));

    }
}      

對于以上的規律,進行優化

/**
 * @author xiexu
 * @create 2020-11-05 11:17 上午
 */
public class ShellSort {

    public static void main(String[] args) {
        int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
        shellSort(arr);
    }

    //使用循環處理
    public static void shellSort(int[] arr) {
        int temp = 0;
        int count = 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;
                    }
                }
            }
            System.out.println("第"+(++count)+"輪排序:" + Arrays.toString(arr));

        }
    }
}      

希爾排序-交換法的速度測試

/**
 * @author xiexu
 * @create 2020-11-05 11:17 上午
 */
public class ShellSort {

    public static void main(String[] args) {
        int[] arr = new int[80000];
        for(int i = 0;i < 80000;i++){
            arr[i] = (int)(Math.random() * 80000); //自動生成[0,80000)之間的随機數
        }

        // 排序前的時間:
        Date data = new Date();
        SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS = simt.format(data);
        System.out.println("排序前的時間是:" + dateS);

        shellSort(arr); // 調用[交換式]排序

        // 排序後的時間:
        Date data2 = new Date();
        SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS2 = simt2.format(data2);
        System.out.println("排序後的時間是:" + dateS2);
    }

    //使用逐漸推導的方式來編寫希爾排序
    public static void shellSort(int[] arr) {
        int temp = 0;
        int count = 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;
                    }
                }
            }

        }
    }
}      
前面的希爾排序代碼都是采用​

​交換式的​

​​,接下來使用​

​移位式​

​排序
/**
 * @author xiexu
 * @create 2020-11-05 11:17 上午
 */
public class ShellSort {

    public static void main(String[] args) {
        int[] arr = new int[80000];
        for (int i = 0; i < 80000; i++) {
            arr[i] = (int) (Math.random() * 80000); //自動生成[0,80000)之間的随機數
        }

        // 排序前的時間:
        Date data = new Date();
        SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS = simt.format(data);
        System.out.println("排序前的時間是:" + dateS);

        //shellSort(arr); // 調用[交換式]排序
        shellSort2(arr); //調用[移位式]排序

        // 排序後的時間:
        Date data2 = new Date();
        SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS2 = simt2.format(data2);
        System.out.println("排序後的時間是:" + dateS2);
    }

    //使用逐漸推導的方式來編寫希爾排序
    public static void shellSort(int[] arr) {
        int temp = 0;
        int count = 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;
                    }
                }
            }

        }
    }

    //對交換式的希爾排序進行優化 -> 移位法
    public static void shellSort2(int[] arr) {
        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[j];
                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;
                }
            }
        }
    }
}      

7.快速排序

  • 同冒泡排序一樣,快速排序也屬于交換排序,通過元素之間的比較和交換位置來達到排序的目的。
  • 不同的是,冒泡排序在每一輪中隻把一個元素冒泡到數列的一端,而快速排序則在每一輪挑選一個基準元素,并讓其他比它大的元素移動到數列的一邊,比它小的元素則移動到數列的另一邊,進而把數列拆解成兩個部分。

7.1 代碼實作

/**
 * @author xiexu
 * @create 2020-11-05 10:52 下午
 */
public class QuickSort {

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

    public static void quackSort(int[] arr, int startIndex, int endIndex) {
        //遞歸結束的條件:startIndex >= endIndex時
        if (startIndex >= endIndex) {
            return;
        }
        //得到基準元素位置
        int pivotIndex = partition(arr,startIndex,endIndex);
        //根據基準元素,分成兩部分進行遞歸排序
        quackSort(arr, startIndex, pivotIndex - 1); //往左邊遞歸
        quackSort(arr, pivotIndex + 1, endIndex); //往右邊遞歸
    }

    /**
     * 分治(雙邊循環法)
     *
     * @param arr
     * @param startIndex
     * @param endIndex
     * @return
     */
    private static int partition(int[] arr, int startIndex, int endIndex) {
        //取第一個位置(也可以選擇随機位置)的元素作為基準元素
        int pivot = arr[startIndex];
        int left = startIndex;
        int right = endIndex;

        while (left != right) {
            //控制right指針比較并左移
            while (left < right && arr[right] > pivot) {
                right--;
            }
            //控制left指針比較并右移
            while (left < right && arr[left] <= pivot) {
                left++;
            }
            //交換left和right指針所指向的元素
            if (left<right) {
                int temp = arr[left];
                arr[left] = arr[right];
                arr[right] = temp;
            }
        }

        //pivot和指針重合點交換
        arr[startIndex] = arr[left];
        arr[left] = pivot;

        return left;
    }

}      

單邊循環法

/**
     * 分治(單邊循環法)
     * @param arr 待交換的數組
     * @param startIndex 起始下标
     * @param endIndex 結束下标
     * @return
     */
    private static int partition2(int[] arr, int startIndex, int endIndex) {
        int pivot = arr[startIndex];
        int mark = startIndex;

        for (int i = startIndex+1; i <= endIndex; i++) {
            if(arr[i] < pivot) {
                mark++;
                int temp = arr[mark];
                arr[mark] = arr[i];
                arr[i] = temp;
            }
        }

        arr[startIndex] = arr[mark];
        arr[mark] = pivot;
        return mark;

    }      

對快速排序進行速度測試

/**
 * @author xiexu
 * @create 2020-11-05 10:52 下午
 */
public class QuickSort {

    public static void main(String[] args) {
        int[] arr = new int[80000];
        for (int i = 0; i < 80000; i++) {
            arr[i] = (int) (Math.random() * 80000); //自動生成[0,80000)之間的随機數
        }

        // 排序前的時間:
        Date data = new Date();
        SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS = simt.format(data);
        System.out.println("排序前的時間是:" + dateS);

        quackSort(arr,0,arr.length-1);

        // 排序後的時間:
        Date data2 = new Date();
        SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS2 = simt2.format(data2);
        System.out.println("排序後的時間是:" + dateS2);
    }

    public static void quackSort(int[] arr, int startIndex, int endIndex) {
        //遞歸結束的條件:startIndex >= endIndex時
        if (startIndex >= endIndex) {
            return;
        }
        //得到基準元素位置
        int pivotIndex = partition(arr, startIndex, endIndex); //雙邊循環
        //int pivotIndex = partition2(arr, startIndex, endIndex); //單邊循環
      
        //根據基準元素,分成兩部分進行遞歸排序
        quackSort(arr, startIndex, pivotIndex - 1);
        quackSort(arr, pivotIndex + 1, endIndex);
    }

    /**
     * 分治(雙邊循環法)
     * @param arr
     * @param startIndex
     * @param endIndex
     * @return
     */
    private static int partition(int[] arr, int startIndex, int endIndex) {
        //取第一個位置(也可以選擇随機位置)的元素作為基準元素
        int pivot = arr[startIndex];
        int left = startIndex;
        int right = endIndex;

        while (left != right) {
            //控制right指針比價并左移
            while (left < right && arr[right] > pivot) {
                right--;
            }
            while (left < right && arr[left] <= pivot) {
                left++;
            }
            //交換left和right指針所指向的元素
            if (left < right) {
                int temp = arr[left];
                arr[left] = arr[right];
                arr[right] = temp;
            }
        }

        //pivot和指針重合點交換
        arr[startIndex] = arr[left];
        arr[left] = pivot;

        return left;
    }

    /**
     * 分治(單邊循環法)
     * @param arr 待交換的數組
     * @param startIndex 起始下标
     * @param endIndex 結束下标
     * @return
     */
    private static int partition2(int[] arr, int startIndex, int endIndex) {
        int pivot = arr[startIndex];
        int mark = startIndex;

        for (int i = startIndex+1; i <= endIndex; i++) {
            if(arr[i] < pivot) {
                mark++;
                int temp = arr[mark];
                arr[mark] = arr[i];
                arr[i] = temp;
            }
        }

        arr[startIndex] = arr[mark];
        arr[mark] = pivot;
        return mark;
    }

}      

8.歸并排序

  • 歸并排序(MERGE-SORT)是利用歸并的思想實作的排序方法,該算法采用經典的分治(divide-and-conquer)政策(分治法将問題分(divide)成一些小的問題然後遞歸求解,而​

    ​治(conquer)​

    ​​的階段則将分的階段得到的各答案​

    ​修補​

    ​在一起,即分而治之)。
歸并排序思想示意圖1-基本思想
徹底搞懂排序算法,看這篇就夠了
說明

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

歸并排序思想示意圖2-合并相鄰有序子序列

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

徹底搞懂排序算法,看這篇就夠了
徹底搞懂排序算法,看這篇就夠了
歸并排序的動态圖
徹底搞懂排序算法,看這篇就夠了

8.1 代碼實作

/**
 * @author xiexu
 * @create 2020-11-07 4:26 下午
 */
public class MergetSort {

    public static void main(String[] args) {
        int[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
        int temp[] = new int[arr.length]; //歸并排序需要一個額外空間
        mergeSort(arr, 0, arr.length - 1, temp);

        System.out.println("歸并排序的結果:" + Arrays.toString(arr));
    }

    //分+和的方法
    public static void mergeSort(int[] arr,int left,int right,int[] tem){
        if(left < right){
            int mid = (left + right) / 2;    //中間索引
            //向左進行遞歸
            mergeSort(arr, left, mid, tem);
            //向右遞歸
            mergeSort(arr, mid + 1, right, tem);
            //合并
            marge(arr, left, mid, right, tem);
        }
    }

    //合并的方法
    public static void marge(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) { //繼續
            //如果左邊的有序序列的目前元素,小于等于右邊有序序列的目前元素
            //即:将左邊的目前元素,填充到temp數組
            //然後 t++, i++
            if (arr[i] < arr[j]) {
                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 += 1;
            i += 1;
        }
        while(j <= right){    //右邊的有序序列還有剩餘的元素,就全部填充到temp數組
            temp[t] = arr[j];
            t += 1;
            j += 1;
        }

        //三、
        //将temp數組的元素拷貝到arr,注意,并不是每次都拷貝所有資料
        t = 0;
        int tempLeft = left;
        //第一次合并 tempLeft = 0 , right = 1
        //第二次合并 tempLeft = 2  right = 3
        //第三次合并 tempLeft = 0 right=3
        //最後一次合并 tempLeft = 0  right = 7
        System.out.println("tempLeft = " + tempLeft + ", right = " + right);
        while(tempLeft <= right){
            arr[tempLeft] = temp[t];
            t += 1;
            tempLeft += 1;
        }

    }
}      

對歸并排序進行速度測試

/**
 * @author xiexu
 * @create 2020-11-07 4:26 下午
 */
public class MergetSort {

    public static void main(String[] args) {
        int[] arr = new int[80000];
        for(int i = 0;i < 80000;i++){
            arr[i] = (int)(Math.random() * 80000); //自動生成[0,80000)之間的随機數
        }

        // 排序前的時間:
        Date data = new Date();
        SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS = simt.format(data);
        System.out.println("排序前的時間是:" + dateS);

        int temp[] = new int[arr.length]; //歸并排序需要一個額外空間
        mergeSort(arr, 0, arr.length - 1, temp);

        // 排序後的時間:
        Date data2 = new Date();
        SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS2 = simt2.format(data2);
        System.out.println("排序後的時間是:" + dateS2);
    }

    //分+和的方法
    public static void mergeSort(int[] arr,int left,int right,int[] tem){
        if(left < right){
            int mid = (left + right) / 2;    //中間索引
            //向左進行遞歸
            mergeSort(arr, left, mid, tem);
            //向右遞歸
            mergeSort(arr, mid + 1, right, tem);
            //合并
            marge(arr, left, mid, right, tem);
        }
    }

    //合并的方法
    public static void marge(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) { //繼續
            //如果左邊的有序序列的目前元素,小于等于右邊有序序列的目前元素
            //即:将左邊的目前元素,填充到temp數組
            //然後 t++, i++
            if (arr[i] < arr[j]) {
                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 += 1;
            i += 1;
        }
        while(j <= right){    //右邊的有序序列還有剩餘的元素,就全部填充到temp數組
            temp[t] = arr[j];
            t += 1;
            j += 1;
        }

        //三、
        //将temp數組的元素拷貝到arr,注意,并不是每次都拷貝所有資料
        t = 0;
        int tempLeft = left;
        //第一次合并 tempLeft = 0 , right = 1
        //第二次合并 tempLeft = 2  right = 3
        //第三次合并 tempLeft = 0 right=3
        //最後一次合并 tempLeft = 0  right = 7
        //System.out.println("tempLeft = " + tempLeft + ", right = " + right);
        while(tempLeft <= right){
            arr[tempLeft] = temp[t];
            t += 1;
            tempLeft += 1;
        }

    }
}      

9.基數排序

  • 基數排序(radix sort)屬于“配置設定式排序”(distribution sort),又稱​

    ​桶子法​

    ​(bucket sort)或bin sort,顧名思義,它是通過鍵值的各個位的值,将要排序的元素配置設定至某些“桶”中,達到排序的作用
  • 基數排序法是屬于​

    ​穩定性​

    ​​的排序,基數排序法的是​

    ​效率高​

    ​​的​

    ​穩定性排序法​

  • 基數排序(Radix Sort)是​

    ​桶排序​

    ​的擴充
  • 基數排序是1887年赫爾曼·何樂禮發明的。它是這樣實作的:将整數按位數切割成不同的數字,然後按每個位數分别比較。
基數排序的基本思想
  • 将所有待比較數值統一為同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後, 數列就變成一個有序序列。
  • 這樣說明,比較難了解,下面我們看一個圖文解釋,了解基數排序的步驟
将數組 {53, 3, 542, 748, 14, 214} 使用基數排序, 進行升序排序。
徹底搞懂排序算法,看這篇就夠了
徹底搞懂排序算法,看這篇就夠了
徹底搞懂排序算法,看這篇就夠了
基數排序動态圖
徹底搞懂排序算法,看這篇就夠了

9.1 代碼實作

/**
 * @author xiexu
 * @create 2020-11-09 10:26 上午
 */
public class RadixSort {
    public static void main(String[] args) {
        int[] arr = {53, 3, 542, 748, 14, 214};
        radSort(arr);
    }

    public static void radSort(int[] arr) {
        //定義一個二維數組,表示10個桶,每個桶即為一個一維數組
        //說明:
        //1.二維數組包含10個一維數組
        //2.為了防止在放入數的時候,資料溢出,則每個一維數組(桶),大小定為arr.length
        //3.需要明确:基數排序是使用空間換時間的經典算法
        int[][] bucket = new int[10][arr.length];

        //為了記錄每個桶中,實際存放了多少個資料,需要定義一個一維數組來記錄各個桶 每次放入的資料個數
        //可以這裡了解
        //比如:bucketNums[0],記錄的就是:bucket[0]桶 放入資料的個數
        int[] bucketNums = new int[10];

        //第1輪排序(對每個元素的個位進行排序)
        for (int j = 0; j < arr.length; j++) {
            //取出每個元素的個位的值
            int dig = arr[j] / 1 % 10; //例如:748 % 10 = 8
            //放入到對應的桶中
            bucket[dig][bucketNums[dig]] = arr[j];
            bucketNums[dig]++;
        }
        //按照這個桶的順序(從一維數組的下标依次取出資料,放入原來數組)
        int index = 0;
        //周遊每一個桶,并将桶中的資料,放入原數組中
        for (int k = 0; k < bucketNums.length; k++) {
            //如果桶中有資料,才放入原數組
            if (bucketNums[k] != 0) {
                //循環該桶,即第k個桶(即第k個一維數組),放入資料
                for (int l = 0; l < bucketNums[k]; l++) {
                    //取出資料放入到arr中
                    arr[index++] = bucket[k][l];
                }
            }
            //第1輪處理後,需要将每個 bucketNums[k] = 0
            bucketNums[k] = 0; //重置為0
        }
        System.out.println("第1輪排序:" + Arrays.toString(arr));

        //-----------------------------------------------------

        //第2輪排序(對每個元素的十位進行排序)
        for (int j = 0; j < arr.length; j++) {
            //取出每個元素的十位的值
            int dig = arr[j] / 10 % 10; //例如:748 / 10 = 74 ; 74 % 10 = 4
            //放入到對應的桶中
            bucket[dig][bucketNums[dig]] = arr[j];
            bucketNums[dig]++;
        }
        //按照這個桶的順序(從一維數組的下标依次取出資料,放入原來數組)
        index = 0;
        //周遊每一個桶,并将桶中的資料,放入原數組中
        for (int k = 0; k < bucketNums.length; k++) {
            //如果桶中有資料,才放入原數組
            if (bucketNums[k] != 0) {
                //循環該桶,即第k個桶(即第k個一維數組),放入資料
                for (int l = 0; l < bucketNums[k]; l++) {
                    //取出資料放入到arr中
                    arr[index++] = bucket[k][l];
                }
            }
            //第2輪處理後,需要将每個 bucketNums[k] = 0
            bucketNums[k] = 0; //重置為0
        }
        System.out.println("第2輪排序:" + Arrays.toString(arr));

        //-----------------------------------------------------

        //第3輪排序(對每個元素的百位進行排序)
        for (int j = 0; j < arr.length; j++) {
            //取出每個元素的百位的值
            int dig = arr[j] / 100 % 10; //例如:748 / 100 = 7
            //放入到對應的桶中
            bucket[dig][bucketNums[dig]] = arr[j];
            bucketNums[dig]++;
        }
        //按照這個桶的順序(從一維數組的下标依次取出資料,放入原來數組)
        index = 0;
        //周遊每一個桶,并将桶中的資料,放入原數組中
        for (int k = 0; k < bucketNums.length; k++) {
            //如果桶中有資料,才放入原數組
            if (bucketNums[k] != 0) {
                //循環該桶,即第k個桶(即第k個一維數組),放入資料
                for (int l = 0; l < bucketNums[k]; l++) {
                    //取出資料放入到arr中
                    arr[index++] = bucket[k][l];
                }
            }
            //第3輪處理後,需要将每個 bucketNums[k] = 0
            bucketNums[k] = 0; //重置為0
        }
        System.out.println("第3輪排序:" + Arrays.toString(arr));
    }

}      

對于以上的規律,進行優化

/**
 * @author xiexu
 * @create 2020-11-09 10:26 上午
 */
public class RadixSort {
    public static void main(String[] args) {
        int[] arr = {53, 3, 542, 748, 14, 214};
        radSort(arr);
    }

    public static void radSort(int[] arr) {

        //根據前面的推導過程,我們可以得到最終的基數排序代碼

        //得到數組中的最大數
        int max = arr[0]; //假設第一個數為最大數
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        //計算出最大數是幾位數
        int maxLen = (max + "").length();

        //定義一個二維數組,表示10個桶,每個桶即為一個一維數組
        //說明:
        //1.二維數組包含10個一維數組
        //2.為了防止在放入數的時候,資料溢出,則每個一維數組(桶),大小定為arr.length
        //3.需要明确:基數排序是使用空間換時間的經典算法
        int[][] bucket = new int[10][arr.length];

        for (int i = 0, n = 1; i < maxLen; i++, n *= 10) {

            //為了記錄每個桶中,實際存放了多少個資料,需要定義一個一維數組來記錄各個桶 每次放入的資料個數
            //可以這裡了解
            //比如:bucketNums[0],記錄的就是:bucket[0]桶 放入資料的個數
            int[] bucketNums = new int[10];

            //第1輪排序(對每個元素的個位進行排序)
            for (int j = 0; j < arr.length; j++) {
                //取出每個元素的個位的值
                int dig = arr[j] / n % 10; //例如:748 % 10 = 8
                //放入到對應的桶中
                bucket[dig][bucketNums[dig]] = arr[j];
                bucketNums[dig]++;
            }
            //按照這個桶的順序(從一維數組的下标依次取出資料,放入原來數組)
            int index = 0;
            //周遊每一個桶,并将桶中的資料,放入原數組中
            for (int k = 0; k < bucketNums.length; k++) {
                //如果桶中有資料,才放入原數組
                if (bucketNums[k] != 0) {
                    //循環該桶,即第k個桶(即第k個一維數組),放入資料
                    for (int l = 0; l < bucketNums[k]; l++) {
                        //取出資料放入到arr中
                        arr[index++] = bucket[k][l];
                    }
                }
                //第1輪處理後,需要将每個 bucketNums[k] = 0
                bucketNums[k] = 0; //重置為0
            }
            System.out.println("第" + (i + 1) + "輪排序:" + Arrays.toString(arr));
        }
    }

}      

對基數排序進行速度測試

/**
 * @author xiexu
 * @create 2020-11-09 10:26 上午
 */
public class RadixSort {
    public static void main(String[] args) {
        int[] arr = new int[8000000];
        for (int i = 0; i < 8000000; i++) {
            arr[i] = (int) (Math.random() * 8000000); //自動生成[0,8000000)之間的随機數
        }

        // 排序前的時間:
        Date data = new Date();
        SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS = simt.format(data);
        System.out.println("排序前的時間是:" + dateS);

        radSort(arr);    //調用基數排序

        // 排序後的時間:
        Date data2 = new Date();
        SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateS2 = simt2.format(data2);
        System.out.println("排序後的時間是:" + dateS2);
    }

    public static void radSort(int[] arr) {

        //根據前面的推導過程,我們可以得到最終的基數排序代碼

        //得到數組中的最大數
        int max = arr[0]; //假設第一個數為最大數
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        //計算出最大數是幾位數
        int maxLen = (max + "").length();

        //定義一個二維數組,表示10個桶,每個桶即為一個一維數組
        //說明:
        //1.二維數組包含10個一維數組
        //2.為了防止在放入數的時候,資料溢出,則每個一維數組(桶),大小定為arr.length
        //3.需要明确:基數排序是使用空間換時間的經典算法
        int[][] bucket = new int[10][arr.length];

        for (int i = 0, n = 1; i < maxLen; i++, n *= 10) {

            //為了記錄每個桶中,實際存放了多少個資料,需要定義一個一維數組來記錄各個桶 每次放入的資料個數
            //可以這裡了解
            //比如:bucketNums[0],記錄的就是:bucket[0]桶 放入資料的個數
            int[] bucketNums = new int[10];

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

            //System.out.println("第" + (i + 1) + "輪排序:" + Arrays.toString(arr));
        }
    }
}      

9.2 基數排序算法的注意事項

  • 基數排序是對傳統​

    ​桶排序​

    ​​的擴充,​

    ​速度很快​

  • 基數排序是經典的空間換時間的方式,占用記憶體很大,當對海量資料排序時,容易造成​

    ​OutOfMemoryError​

    ​。
  • 基數排序是​

    ​穩定的​

    ​​。[注:假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,​

    ​r[ i ]=r[ j ],且r[ i ]在r[ j ]之前,而在排序後的序列中,r[ i ]仍在r[ j ]之前​

    ​,則稱這種排序算法是穩定的;否則稱為不穩定的]
  • 有負數的數組,我們不用基數排序來進行排序,如果要支援負數,參考:https://code.i-harness.com/zh-CN/q/e98fa9

10.堆排序(在二叉樹部分)

11.常用排序算法的總結和對比

  • 穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面
  • 不穩定:如果a原本在b的前面,而a=b,排序之後a可能會出現在b的後面
  • 内排序:所有排序操作都在記憶體中完成
  • 外排序:由于資料太大,是以把資料放在磁盤中,而排序通過磁盤和記憶體的資料傳輸才能進行
  • 時間複雜度:一個算法執行所耗費的時間
  • 空間複雜度:運作完一個程式所需記憶體的大小
  • n:資料規模
  • k:​

    ​桶​

    ​的個數
  • In-place:不占用額外記憶體
  • Out-place:占用額外記憶體

繼續閱讀