常見的6種排序圖解 - java語言描述

目錄

1、冒泡排序

2、直接插入排序

3. 希爾排序

4、歸併排序

5. 快速排序

6. 選擇排序


首先,在說幾個排序算法之前,先自己寫一個簡單的工具類,判斷一個數列是否有序(以升序爲例),如果不是升序的數列,在出現亂序的地方把附近的兩個元素輸出一下:

 /**
     * 判斷一組數據是不是升序
     * @param array  傳入一個需要判斷是否有序的數列
     */
    public static void isSortedAsc(int[] array)
    {
        for (int i=1; i<array.length; i++)
        {
            if (array[i] < array[i-1]){
                System.out.println("array["+(i-1)+"] = "+array[i-1]);
                System.out.println("array["+i+"] = "+array[i]);
                System.out.println("數據不是升序");
                return;
            }
        }
        System.out.println("數據是升序");
    }

運行結果如下:

以下的排序都以升序爲例......

在寫一個主方法,用來測試排序的結果:

public static void main(String[] args) {
        // 隨機生成10000個數 對他們進行排序
        int[] array = new int[10000];
        Random random = new Random();
        for (int i = 0; i < array.length; i++) {
            array[i] = random.nextInt(10000)+1;
        }
        System.out.println("排序前:" + Arrays.toString(array));
        // 記錄時間
        long start = System.currentTimeMillis();
        // 這裏調用具體的排序方法
        sort(array);
        long end = System.currentTimeMillis();
        System.out.println("運行時間:"+(end-start));
        System.out.println("排序後:"+Arrays.toString(array));
        SortUtils.isSortedAsc(array);
    }

1、冒泡排序

冒泡排序是幾種排序算法中最常見的一種,其思想就是:給定一組數據,然後按順序兩兩數字進行比較,如果前面的一個元素比後面的大,就讓他們兩個進行交換,然後繼續朝後比較,從數列最左邊走到數列最右邊的過程稱爲一趟冒泡排序,經過一趟冒泡排序,會把最大的元素放到數列的最右邊。如下圖

當一趟冒泡排序走完之後,最大的數就放到了最右邊,然後現在我們就可以直接對剩下的9個數再進行冒泡排序:

當這一趟冒泡排序結束之後,第二大的數就會被挪到倒數第二個位置,如下圖:

通過以上分析,冒泡排序代碼如下:

public static void bubbleSort(int[] array){
        // 外層循環表示的是變量j一共要進行多少趟冒泡排序
        for (int i=0; i<array.length-1; i++) {
            // 定義一個標誌,默認爲真
            boolean flag = true;
            for (int j=0; j<array.length-i-1; j++)
            {
                if (array[j] > array[j+1])
                {
                    int tmp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = tmp;
                    // 每趟排序之後又數據交換,就說明數組還不是有序的,就把標誌設置爲假
                    flag = false;
                }
            }
            if (flag) {
                // 如果其中某一趟排序的時候發現沒有一個數據交換,就說明數組已經有序了,就不用再進行排序了,可以直接退出循環
                break;
            }
        }
    }

使用上面的兩個測試類進行測試:

冒泡排序分析:

  • 時間複雜度:O(n^2)
  • 空間複雜度:O(1)
  • 穩定性 :穩定

2、直接插入排序

思想:兩個下標 i, j,一個臨時變量tmp,i用來遍歷待排序序列,tmp存放arr[i],j=i-1,j朝0的方向遞減,將tmp中的值插入合適的位置。直接插入排序的思想就和我們打撲克牌的情況一樣,當我們拿到第一張牌的時候,由於此時手上只有一張牌,所以它一定是最小(或最大)的一張,當我們接到第二張牌的時候,如果它比第一張牌小,我們就把它放第一張牌的前邊,反之,就放到它後邊,以此類推。現在給我們一個數組,我們就默認它第一個元素一定是有序的,從第二個元素開始,如果第二個元素比第一個大,就不用管他,如果第二個元素小於第一個元素,就把第二個元素放第一個前面去,這時前兩個元素就有序了;然後到了第三個元素,如果第三個元素大於第二個元素,那麼第三個元素一定大於第一個元素,也就不用管它,如果它比第二個元素小,就把第二個元素挪到第三個元素的位置,然後第三個和第一個在比較,如果第三個元素大於第一個,就把它放第一個後面(第二個的位置),如果第三個小於第一個,就把它放第一個前面,以此類推,過程如下圖:

圖解: 這裏我們定義一個臨時變量tmp存儲當前元素

代碼:

public static void insertSort(int[] array){
        // 從第二個元素開始 一次朝後走
        for(int i=1; i<array.length; i++)
        {
            int tmp = array[i] ;
            int j = i-1 ;
            // j 從 i-1的位置開始朝回遍歷,將tmp插入到合適的位置
            while (j>=0 && array[j] > tmp )
            {
                array[j+1] = array[j];
                j--;
            }
            array[j+1] = tmp;
        }
    }

測試結果:

直接插入排序總結:

  • 時間複雜度:O(n)
  • 空間複雜度:O(1)
  • 穩定性 :穩定
  • 適用場景:適合用來排一些接近有序的數列,因爲如果數列有序,直接從左到右走一遍就行了,時間複雜度爲O(n),但是如果亂序或倒序,每走一步,它都會在往回走,把當前元素插入到合適的位置在往後走。

3.希爾排序

思想:希爾排序是對直接插入排序的一種優化;採用的是將數據分組,然後每組在組內進行直接插入排序

爲什麼要分組呢:試想一下加入現在有10000個數據要進行直接插入排序,時間複雜度爲 10000 * 10000 = 10000 0000 ;但是如果將這組數據分成100組 , 每組進行插入排序,時間複雜度就變成了 100 * 100 * 100 = 1 00 00 00 。直接少了兩個0 ......

那麼要怎麼分組呢:如果是讓我們這種普通人來分組,就下面這組數據,要平均分爲兩種,我們一開始想到的肯定是這樣

但是人家科學家分組就不一樣了,他們分組是這麼分的:

那麼這麼分組有什麼好處呢:加入我們對下面這組數據每組的組內進行直接插入排序:

組內排序之後,我們會發現,每一組在組內都是有序的,也就是說,每一組的左邊的數據都比較小,右邊的數據都比較大,整體也是一樣,這樣的話我們就可以認爲整個數組是接近有序的,上面已經說過,直接插入排序適合用來排接近有序的數列,所以現在在對整體進行直接插入排序就會快很多。

所以希爾排序的本質還是直接插入排序。

代碼:

public static void shellSort(int[] array, int gap) {
        // i從gap的位置開始
        for (int i=gap; i<array.length; i++)
        {
            // j的初始值爲i-gap
            int j = i - gap;
            // 定義一個臨時變量存儲array[i]的值
            int tmp = array[i];
            // j從i減gap的位置開始朝回遍歷,吧tmp放在合適的位置
            while (j>=0 && array[j] > tmp)
            {
                array[j+gap] = array[j];
                j -= gap;
            }
            array[j+gap] = tmp;
        }
    }

希爾排序分析:

  • 時間複雜度:O(n^1.3 - n^1.5)
  • 空間複雜度:O(1)
  • 穩定性:不穩定

4、歸併排序

思想:歸併排序的思想同樣是將一個大的數列劃分成單個有序的數據(之後一個元素的數列一定是有序的),然後在把有序的小數列合併成一個大的有序數列,從而實現整體有序的結果,過程如下圖:

代碼:

private static void mergeSort(int[] array, int start, int end) {
        int mid = (start+end)/2 ;
        if (end == start)
        {
           return ;
        }
        // 朝左邊拆分
        mergeSort(array, start, mid);
        // 朝右邊拆分
        mergeSort(array, mid+1, end);
        // 拆分成單個的之後,歸併
        merge(array, start, mid, end);
    }

    private static void merge(int[] array, int start, int mid, int end) {
        int[] tmpArray = new int[array.length] ;
        int i = start ; // 保留一個start的值,最後拷貝數組的時候使用
        int start1 = start; 
        int end1 = mid;
        int start2 = mid+1;
        int end2 = end;
        while (start1 <= end1 && start2 <= end2)
        {
            // 哪一個數組中的值小把哪一個的值拿到新數組中
            if (array[start1] <= array[start2]){
                tmpArray[start++] = array[start1++];
            }else {
                tmpArray[start++] = array[start2++];
            }
        }
        // 如果第一個數組還沒有走完
        while (start1 <= end1)
        {
            tmpArray[start++] = array[start1++];
        }
        // 如果第二個數組還沒有走完
        while (start2 <= end2)
        {
            tmpArray[start++] = array[start2++];
        }
        // 數組拷貝
        for ( ; i<=end; i++)
        {
            array[i] = tmpArray[i];
        }
    }

運行結果:

優化:

但是這麼寫仔細觀察會發現有一點問題,因爲每次遞歸都會創建一個和array一樣大的數組,但是隻用了其中的一小部分,所以可以將tmpArray在排序之前只創建一次,然後作爲參數傳遞就可以了,不需要在遞歸中創建。

private static void mergeSort_R(int[] array, int start, int end, int[] tmpArr)
    {
        if (start < end)
        {
            int mid = (start+end)/2 ;
            mergeSort_R(array, start, mid, tmpArr);
            mergeSort_R(array, mid+1, end, tmpArr);
            merge(array, start, mid, end, tmpArr);
        }
    }

    private static void merge(int[] array, int start, int mid, int end, int[] tmpArr) {
        int pos = start;
        int index = start;
        int start2 = mid+1 ;
        while (start <= mid && start2 <= end){
            if (array[start] < array[start2]) {
                tmpArr[index++] = array[start++];
            }else {
                tmpArr[index++] = array[start2++];
            }
        }
        while (start <= mid){
            tmpArr[index++] = array[start++];
        }
        while (start2 <= end){
            tmpArr[index++] = array[start2++];
        }
        // 數組拷貝
        for(int i=pos; i<=end; i++)
        {
            array[i] = tmpArr[i];
        }
    }

 

5. 快速排序

思想:和上面那種歸併排序的思想類似,都是將一個大的問題劃分爲多個小問題,快速排序是 :先選取一個基準值,每經過一趟冒泡排序,就可以將比基準大的放在基準值的右邊,比它小的放它左邊。然後在分別對基準值左邊的和基準值右邊的分別進行冒泡排序。直到整個數列有序。

那麼現在主要的問題就是怎麼將一個數組根據基準值分爲兩部分,使左邊的小於基準值,右邊的大於基準值。通常的方法是挖坑法

下面,再結合代碼看一下這個 ‘挖坑’ 的過程:

 private static int partion(int[] arr, int left, int right)
    {
        // 保存第一個位置的元素
        int tmp = arr[left];
        while (left != right)
        {
            // 右邊的和tmp比較 如果右邊的比tmp大,就不用管,繼續朝左邊走 
            // 注意這裏得有等號,不然如果左邊等於tmp,右邊等於tmp,會出現死循環,下面等號也是一樣
            while (left != right && arr[right] >= tmp)
            {
                right--;
            }
            // 如果右邊的數小於tmp,就把右邊的數賦給左邊的數
            arr[left] = arr[right];
            // 左邊的數和tmp比較,如果左邊的數大於tmp 就把左邊的數賦值給右邊的數
            while (left != right && arr[left] <= tmp){
                left++ ;
            }
            arr[right]= arr[left];
        }
        // 當left == right 把tmp的值賦給當前位置
        arr[left] = tmp;
        // 返回位置下標
        return left;
    }

最後通過遞歸的方式,讓返回位置的左邊也進行快速排序,右邊也進行快速排序:

public static void quickSort (int[] arr, int left, int rigth){
        // 獲取關鍵字的位置
        int mid = partion(arr, left, rigth);
        // 如果關鍵字左邊還剩一個元素,就不用比了
        if (mid-left > 1){
            quickSort(arr, left, mid-1);
        }
        // 如果右邊還剩一個元素,也就不用比了
        if (rigth-mid > 1){
            quickSort(arr, mid+1, rigth);
        }
    }

結果測試:

快排優化1 --- 三數取中法

但是這樣的快排還是有一些問題,假如每次排序數列第一個剛好是最大的或最小的,就會使快速排序時間複雜度退化爲O(n^2),因此需要對這個快速排序進行優化,常見的優化方式有:隨機取基法,三數取中法

  • 隨機取基法:就是隨機取一個數作爲基數,這樣就可以避免取到最大或最小的數作爲基數,但是由於是隨機的,所以這種方法完全是一種看人品的方法,所以不推薦使用
  • 三數取中法:就是在第一個數,中間位置的數,最後位置的數 ,在這三個數中,取這三個數(按大小排序)的中間值作爲基準值,然後在進行快速排序,這樣可以保證至少每次取得基準值都不會是最大或最小的一個數,下面具體說說這種方法:

至於三數取中的實現,就有很多方式了,因爲只是對3個數的比較,可以使用if語句比較大小然後交換值,也可以將這三個數排序然後按順序賦值等方法:

/**
     * 快排優化1
     *     三數取中法 : 最後要達到的效果:array[mid] < array[low] <array[high]
     */
    public static void medianOfThree(int[] array, int low, int high)
    {
        int mid = (low+high)/2 ;
        int[] tmps = new int[]{array[mid], array[low], array[high]};
        Arrays.sort(tmps);
        array[mid] = tmps[0];
        array[low] = tmps[1];
        array[high] = tmps[2];
    }

然後在每次要進行快排的操作時,都調用一下三數取中的方法即可;

快排優化2 :使用插入排序

當使用快排對數列進行排序時,排到一定程度數據就接近有序了,此時可以不必在使用遞歸的快排,而使用插入排序對數列進行排序:

public static void sort(int[] array, int start, int end){
        // 優化方式1:先用三數取中法
        medianOfThree(array, start, end);

        // 優化方式2:當一個區間中剩餘數的個數小於等於16的時候,我們就認爲這個區間的數已經接近有序了,次數可以使用插入排序接着排
        if (end-start+1 <= 16) // end-start+1 表示start到end之間的元素個數
        {
            insertSort(array, start, end);
        }
        int par = partion(array, start, end);
        // 遞歸左邊  左邊還剩一個元素就不用排序了
        if (par > start+1) {
            sort(array, start, par-1);
        }
        if (par < end-1){
            sort(array, par+1, end);
        }
    }

最後的運行結果:

6. 選擇排序

思想:選擇排序就是先選擇最小的一個數,和第一個數交換,然後在選擇第二小的數,和第二個位置的數交換,以此類推...

代碼實現:

 public static void selectSort(int[] array)
    {
        // 把倒數第二大的數放在倒數第二個位置的時候,最大的數一定在最後的位置,就不用在朝後比較了
        for (int i=0; i<array.length-1; i++){
            // 從i+1d位置開始朝後找,有比i位置的數小的數就交換
            for (int j = i+1; j<array.length; j++){
                if (array[i] > array[j]){
                    int tmp = array[i];
                    array[i] = array[j];
                    array[j] = tmp;
                }
            }
        }
    }

測試結果:

 

 

 

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