算法與數據結構(JAVA)-排序算法總結

冒泡排序

排序思想:通過與相鄰元素的比較和交換來把小的數交換到最前面。這個過程類似於水泡向上升一樣,因此而得名。

例如,對5,3,8,6,4這個無序序列進行冒泡排序。首先從後向前冒泡,4和6比較,把4交換到前面,序列變成5,3,8,4,6。同理4和8交換,變成5,3,4,8,6,3和4無需交換。5和3交換,變成3,5,4,8,6,3.這樣一次冒泡就完了,把最小的數3排到最前面了。對剩下的序列依次冒泡就會得到一個有序序列。

冒泡排序的時間複雜度爲O(n^2)。

/**
 *@Description:<p>冒泡排序算法實現</p>
 *@author FinMo
 *@time 2016-4-25 下午20:00
 */
public class BubbleSort{
    public static void bubbleSort(int[] arr){
        if(arr == NULL||arr.length == 0){
            return ;
        }
        for(int i = 0;i < arr.length - 1;i++){
            for(int j = arr.length - 1;j > i;j--){
                if(arr[j] < arr[j - 1]){
                    swap(arr,j - 1,j);
                }
            }
        }
    }

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

選擇排序

排序思想:和冒泡排序有點類似,都是在一次排序後把最小的元素放到最前面。但是過程不同,冒泡排序是通過相鄰的比較和交換。而選擇排序是通過對整體的選擇。

例如,對5,3,8,6,4這個無序序列進行簡單選擇排序,首先要選擇5以外的最小數來和5交換,也就是選擇3和5交換,一次排序後就變成了3,5,8,6,4.對剩下的序列一次進行選擇和交換,最終就會得到一個有序序列。其實選擇排序可以看成冒泡排序的優化,因爲其目的相同,只是選擇排序只有在確定了最小數的前提下才進行交換,大大減少了交換的次數。

選擇排序的時間複雜度爲O(n^2)。

/**
 *@Description:<p>冒泡排序算法實現</p>
 *@author MoFa
 *@time 2016-4-25 下午20:00
 */
public class SelectSort{
    public static void selectSort(int[] arr){
        if(arr == NULL||arr.length == 0){
            return ;
        }
        int minIndex = 0;
        for(int i = 0;i < arr.length - 1;i++){ //只需要比較n-1次
            minIndex = i;
            for(int j = i + 1;j < arr.length;j++){//從i+1開始比較,因爲minIndex默認爲i了,i就沒有必要比了
                if(arr[j] < arr[minIndex]){
                    minIndex = j;
                }
            }
            if(minIndex != i){ //如果minIndex不爲i,說明找到了更小的值,交換之。
                swap(arr, i, minIndex);
            }
        }
    }
    public static void swap(int[] arr, int i,int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

插入排序

算法思想:插入排序不是通過交換位置而是通過比較找到合適的位置插入元素來達到排序的目的的。相信大家都有過打撲克牌的經歷,特別是牌數較大的。在分牌時可能要整理自己的牌,牌多的時候怎麼整理呢?就是拿到一張牌,找到一個合適的位置插入。這個原理其實和插入排序是一樣的。

例如,對5,3,8,6,4這個無序序列進行簡單插入排序,首先假設第一個數的位置時正確的,想一下在拿到第一張牌的時候,沒必要整理。然後3要插到5前面,把5後移一位,變成3,5,8,6,4.想一下整理牌的時候應該也是這樣吧。然後8不用動,6插在8前面,8後移一位,4插在5前面,從5開始都向後移一位。注意在插入一個數的時候要保證這個數前面的數已經有序。

插入排序的時間複雜度也是O(n^2)。

/**
 *@Description:<p>冒泡排序算法實現</p>
 *@author MoFa
 *@time 2016-4-25 下午20:00
 */
public class InsertSort{
    public static void insertSort(int[] arr){
        if(arr == NULL||arr.length == 0){
            return ;
        }
        for(int i = 1;i < arr.length;i++){//假設第一個數位置正確
            int j = i;
            int target = arr[i];//待插入的數
            //後移    
            while(j > 0&&target < arr[j - 1]){
                arr[j] = arr[j - 1];
                j--;
            }
            //插入
            arr[j] = target;
        }
    }
}

快速排序

算法思想:在實際應用中快速排序是表現最好的排序算法。其思想來自冒泡排序,排序是通過相鄰元素的比較和交換把最小的冒泡到最頂端,而快速排序是比較和交換小數和小數,這樣一來不僅把小數冒泡到上面同時也把大數沉到下面。

例如:對5,3,8,6,4這個無序序列進行快速排序,思路是右指針找比基數小的,左指針找比基準數大的,交換之。

5,3,8,6,4 用5作爲比較的基準,最終會把5小的移動到5的左邊,比5大的移動到5的右邊。

5,3,8,6,4 首先設置i,j兩個指針分別指向兩端,j指針先掃描(*)4比5小停止。然後i掃描,8比5大停止。交換i,j位置。

5,3,4,6,8 然後j指針再掃描,這時j掃描4時兩指針相遇。停止。然後交換4和基準數。

4,3,5,6,8 一次劃分後達到了左邊比5小,右邊比5大的目的。之後對左右子序列遞歸排序,最終得到有序序列。

(*)爲什麼一定要j指針先動呢?首先這也不是絕對的,這取決於基準數的位置,因爲在最後兩個指針相遇的時候,要交換基準數到相遇的位置。一般選取第一個數作爲基準數,那麼就在左邊,所有最後相遇的數和基準數交換,那麼相遇的數一定要比基準數小。所以j指針先移動才能先找到比基準數小的數。

快速排序是不穩定的,其時間平均時間複雜度是O(nlgn)。

/**
 *@Description:<p>冒泡排序算法實現</p>
 *@author MoFa
 *@time 2016-4-25 下午20:00
 */
public class QuickSort{
    //一次劃分
    public static int partition(int[] arr, int left, int right){
        int pivotKey = arr[left];
        int pivorPointer = left;

        while(left < right){
            while(left < right&&arr[right] >= pivotKey){
                right--;
            }
            while(left < right&&arr[left] <=pivotKey){
                left++;
            }
            swap(arr, left, right); //把大的交換到右邊,把小的交換到左邊
        }
        swap(arr, pivotPointer, left); //最後把pivot交換到中間
        return left;
    }
    public static void quickSort(int[] arr, int left, int right){
        if(left >= right){
            return ;
        }
        int pivotPos = partiton(arr, left, right);
        quickSort(arr, left, pivotPos - 1);
        quickSort(arr, pivotPos + 1, right);
    }
    public static void swap(int[] arr, int left, int right){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

其實上面的代碼還可以再優化,上面代碼中基準數已經在pivotKey中保存了,所以不需要每次交換都設置一個temp變量,在交換左右指針的時候只需要先後覆蓋就可以了。這樣既能減少空間的使用還能降低賦值運算的次數。

優化代碼如下:

/**
 *@Description:<p>冒泡排序算法實現</p>
 *@author MoFa
 *@time 2016-4-25 下午20:00
 */
public class QuickSort{
    /**
     * 劃分
     * @param arr
     * @param left
     * @param right
     * @return
     */
     public static int partition(int[] arr, int left, int right){
         int pivotKey = arr[left];
         while(left < right){
             while(left < right&&arr[right] >= pivotKey){
                 right--;
             }
             arr[left] = arr[right]; //把小的移動到左邊
             while(left < right&&arr[left] <=pivotKey){
                 left++;
             }
             arr[right] = arr[left]; //把大的移動到右邊
         }
         arr[left] = pivotKey; //最後把pivot賦值到中間
         return left;
     }
    /**
     * 遞歸劃分子序列
     * @param arr
     * @param left
     * @param right
     */
     public static void quickSort(int[] arr, int left, int right){
        if(left >= right){
            return ;
        }
        int pivotPos = partiton(arr, left, right);
        quickSort(arr, left, pivotPos - 1);
        quickSort(arr, pivotPos + 1, right);
    }
    public static void sort(int[] arr){
        if(arr == null||arr.lengh == 0){
            return ;
        }
        quickSort(arr, 0, arr.length - 1);
    } 
}

總結:快速排序的思想:冒泡+二分+遞歸分治。

堆排序

排序思想:藉助堆來實現的選擇排序,思想同選擇排序。

(以下以大頂堆爲例)

注意:如果想升序排序就使用大頂堆,反之使用小頂堆。原因是堆頂元素需要交換到序列尾部。

首先,實現堆排序需要解決兩個問題:

希爾排序

基本思想是:先將整個待排記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄基本有序時再對全體記錄進行一次直接插入排序。

這裏寫圖片描述

希爾排序的特點是,子序列的構成不是簡單的逐段分割,而是將某個相隔某個增量的記錄組成一個子序列。如上面的例子,第一堂排序時的增量爲5,第二趟排序的增量爲3。由於前兩趟的插入排序中記錄的關鍵字是和同一子序列中的前一個記錄的關鍵字進行比較,因此關鍵字較小的記錄就不是一步一步地向前挪動,而是跳躍式地往前移,從而使得進行最後一趟排序時,整個序列已經做到基本有序,只要作記錄的少量比較和移動即可。因此希爾排序的效率要比直接插入排序高。

希爾排序的分析是複雜的,時間複雜度是所取增量的函數,這涉及一些數學上的難題。但是在大量實驗的基礎上推出當n在某個範圍內時,時間複雜度可以達到O(n^1.3)。

/**
 *@Description:<p>希爾排序算法實現</p>
 *@author MoFa
 *@time 2016-5-6 上午10:53:55
 */
public class ShellSort {

    /**
     * 希爾排序的一趟插入
     * @param arr 待排數組
     * @param d 增量
     */
    public static void shellInsert(int[] arr, int d) {
        for(int i=d; i<arr.length; i++) {
            int j = i - d;
            int temp = arr[i];    //記錄要插入的數據  
            while (j>=0 && arr[j]>temp) {  //從後向前,找到比其小的數的位置   
                arr[j+d] = arr[j];    //向後挪動  
                j -= d;  
            }  

            if (j != i - d)    //存在比其小的數 
                arr[j+d] = temp;            
        }
    }

    public static void shellSort(int[] arr) {
        if(arr == null || arr.length == 0)
            return ;
        int d = arr.length / 2;
        while(d >= 1) {
            shellInsert(arr, d);
            d /= 2;
        }
    }
}  

歸併排序

歸併排序是另一種不同的排序方法,因爲歸併排序使用了遞歸分治的思想,所以理解起來比較容易。其基本思想是,先遞歸劃分子問題,然後合併結果。把待排序列看成由兩個有序的子序列,然後合併兩個子序列,然後把子序列看成由兩個有序序列。倒着來看,其實就是先兩兩合併,然後四四合並。最終形成有序序列。

空間複雜度爲O(n),時間複雜度爲O(nlogn)。

這裏寫圖片描述

/**
 *@Description:<p>歸併排序算法的實現</p>
 *@author MoFa
 *@time 2016-5-6 上午10:53:55
 */
public class MergeSort {

    public static void mergeSort(int[] arr) {
        mSort(arr, 0, arr.length-1);
    }

    /**
     * 遞歸分治
     * @param arr 待排數組
     * @param left 左指針
     * @param right 右指針
     */
    public static void mSort(int[] arr, int left, int right) {
        if(left >= right)
            return ;
        int mid = (left + right) / 2;

        mSort(arr, left, mid); //遞歸排序左邊
        mSort(arr, mid+1, right); //遞歸排序右邊
        merge(arr, left, mid, right); //合併
    }

    /**
     * 合併兩個有序數組
     * @param arr 待合併數組
     * @param left 左指針
     * @param mid 中間指針
     * @param right 右指針
     */
    public static void merge(int[] arr, int left, int mid, int right) {
        //[left, mid] [mid+1, right]
        int[] temp = new int[right - left + 1]; //中間數組

        int i = left;
        int j = mid + 1;
        int k = 0;
        while(i <= mid && j <= right) {
            if(arr[i] <= arr[j]) {
                temp[k++] = arr[i++];
            }
            else {
                temp[k++] = arr[j++];
            }
        }

        while(i <= mid) {
            temp[k++] = arr[i++];
        }

        while(j <= right) {
            temp[k++] = arr[j++];
        }

        for(int p=0; p<temp.length; p++) {
            arr[left + p] = temp[p];
        }

    }
}

總結

在前面的介紹和分析中我們提到了冒泡排序、選擇排序、插入排序三種簡單的排序及其變種快速排序、堆排序、希爾排序三種比較高效的排序。後面我們又分析了基於分治遞歸思想的歸併排序還有計數排序、桶排序、基數排序三種線性排序。我們可以知道排序算法要麼簡單有效,要麼是利用簡單排序的特點加以改進,要麼是以空間換取時間在特定情況下的高效排序。但是這些排序方法都不是固定不變的,需要結合具體的需求和場景來選擇甚至組合使用。才能達到高效穩定的目的。沒有最好的排序,只有最適合的排序。

下面就總結一下排序算法的各自的使用場景和適用場合。

這裏寫圖片描述

1、從平均時間來看,快速排序是效率最高的,但快速排序在最壞情況下的時間性能不如堆排序和歸併排序。而後者相比較的結果是,在n較大時歸併排序使用時間較少,但使用輔助空間較多。

2、上面說的簡單排序包括除希爾排序之外的所有冒泡排序、插入排序、簡單選擇排序。其中直接插入排序最簡單,但序列基本有序或者n較小時,直接插入排序是好的方法,因此常將它和其他的排序方法,如快速排序、歸併排序等結合在一起使用。

3、基數排序的時間複雜度也可以寫成O(d*n)。因此它最使用於n值很大而關鍵字較小的的序列。若關鍵字也很大,而序列中大多數記錄的最高關鍵字均不同,則亦可先按最高關鍵字不同,將序列分成若干小的子序列,而後進行直接插入排序。

4、從方法的穩定性來比較,基數排序是穩定的內排方法,所有時間複雜度爲O(n^2)的簡單排序也是穩定的。但是快速排序、堆排序、希爾排序等時間性能較好的排序方法都是不穩定的。穩定性需要根據具體需求選擇。

5、上面的算法實現大多數是使用線性存儲結構,像插入排序這種算法用鏈表實現更好,省去了移動元素的時間。具體的存儲結構在具體的實現版本中也是不同的。

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