1. 程式人生 > >排序演算法之交換排序(氣泡排序、快速排序)

排序演算法之交換排序(氣泡排序、快速排序)

前言

在前面幾篇部落格中總結了插入排序(直接插入和希爾排序)、選擇排序(直接選擇和堆排序)以及歸併排序,這裡將講下兩種選擇排序演算法——氣泡排序和快速排序。

氣泡排序

基本概念

氣泡排序相對快速排序而言相對簡單。冒泡就如同水裡的魚吐泡泡一樣,剛開始時泡泡很小,但隨著上浮離水面越來越近,泡泡也逐漸變大。氣泡排序也是因此而來,在每一趟排序中,依次比較相鄰的兩個數,選出最大的數將它移到一端,最終將得到一個有序的序列。

實現思路

通過雙重迴圈,外層迴圈控制迴圈次數,內層迴圈控制比較次數。假如有N個數需要進行排序,在第一趟排序過程中,依次比較相鄰兩個數,可通過a[j]和a[j+1]標識,選出最大的數後將其移動至最右端,結束第一次迴圈。開始第二趟排序,此時內層迴圈只需要比較前N-1個元素,因為在上一輪排序中已經選了整個序列中最大的元素放置在最後一位上,直到外層迴圈結束,排序過程完成。

舉例說明

待排序的陣列:int[] array = {6,3,8,2,9,1};

第一趟排序:     第一次排序:6和3比較,6大於3,交換位置:3 6 8 2 9 1          第二次排序:6和8比較,6小於8,不交換位置:3 6 8 2 9 1          第三次排序:8和2比較,8大於2,交換位置:3 6 2 8 9 1          第四次排序:8和9比較,8小於9,不交換位置:3 6 2 8 9 1          第五次排序:9和1比較:9大於1,交換位置:3 6 2 8 1 9          第一趟總共進行了5次比較,選出最大元素9,排序結果:3 6 2 8 1 9

第二趟排序:     第一次排序:3和6比較,3小於6,不交換位置:3 6 2 8 1 9          第二次排序:6和2比較,6大於2,交換位置:3 2 6 8 1 9          第三次排序:6和8比較,6大於8,不交換位置:3 2 6 8 1 9          第四次排序:8和1比較,8大於1,交換位置:3 2 6 1 8 9          第二趟總共進行了4次比較,選出最大元素8,排序結果:3 2 6 1 8 9

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

1 8 9          第三次排序:6和1比較,6大於1,交換位置:2 3 1 6 8 9          第三趟總共進行了3次比較,選出最大元素6,排序結果:2 3 1 6 8 9

第四趟排序:     第一次排序:2和3比較,2小於3,不交換位置:2 3 1 6 8 9          第二次排序:3和1比較,3大於1,交換位置:2 1 3 6 8 9          第四趟總共進行了2次比較,選出最大元素3,排序結果:2 1 3 6 8 9

第五趟排序:     第一次排序:2和1比較,2大於1,交換位置:1 2 3 6 8 9          第五趟總共進行了1次比較,選出最大元素2,排序結果:1 2 3 6 8 9

可以看出,在氣泡排序中,如果有N個元素需要進行排序,則外層迴圈需要進行N-1次,而內層迴圈由於每次會選出一個最大的數,則每次的內層迴圈次數為N-i-1,i為外層迴圈次數。

程式碼解析

/**
 * @author: zhangocean
 * @Date: 2018/11/10 12:51
 */
public class BubbleSort {

    public void bubbleSort(int[] array){
        System.out.println("排序前:" + Arrays.toString(array));
        int temp;
        for(int i=0;i<array.length-1;i++){
            for(int j = 0;j<array.length-i-1;j++){
				//依次比較相鄰元素,並將較大者右移
                if(array[j+1] < array[j]){
                    temp = array[j+1];
                    array[j+1] = array[j];
                    array[j] = temp;
                }
            }
        }
        System.out.println("排序後:" + Arrays.toString(array));
    }

    public static void main(String[] args) {
        int[] arr = new int[10];
        Random random = new Random();
        for(int i=0;i<10;i++){
            arr[i] = random.nextInt(100);
        }
        BubbleSort bubbleSort = new BubbleSort();
        bubbleSort.bubbleSort(arr);

    }
}

輸出結果:

排序前:[83, 79, 16, 44, 41, 0, 1, 22, 19, 44]
排序後:[0, 1, 16, 19, 22, 41, 44, 44, 79, 83]

時間複雜度:O(N²) 空間複雜度:O(1)

快速排序

基本概念

快速排序,也稱為快排。顧名思義,是實踐中的一種快速的排序演算法。該演算法之所以特別快,主要是由於非常精練和高度優化的內部迴圈。在快排中我們首先需要選取一個元素作為樞紐元,然後將待排序的序列中比樞紐元大的數放在一個集合中,再將比樞紐元小的元素放在一個集合中,通過這種方式遞迴的在各個集合中再進行快排,最後將得到一個有序的序列集合。

畫圖分析

首先需要選取一個樞紐元,這裡我們隨機選取65作為樞紐元。 將比樞紐元大的元素以及比樞紐元小的元素各劃分為一個集合,再在集合中重複選取樞紐元以及以上操作,這樣最終將獲得一個有序的序列。

選取樞紐元

在快速排序中,樞紐元的選取十分重要,雖然上面描述的演算法無論選擇哪個元素作為樞紐元都能完成排序工作,但是有些選擇顯然優於其他選擇。

在一些快排演算法中將陣列的第一個元素作為樞紐元,然而在《資料結構與演算法分析for Java》中將這定義為一種錯誤的方法。如果輸入是隨機的,那麼可以接受這種選取方法,而如果輸入是預排序的或是反序的,那麼這樣的樞紐元就產生一個劣質的分割。因為這樣的陣列第一個元素不是最大就是最小的數,那麼必定會導致其他的所有元素被分到同一個集合裡,這樣也失去了快排分割的意義。

這裡使用三數中值分割法來選取樞紐元。在一段待排序的陣列中,我們通常使用左端、右端和中心位置上的三個元素的中值作為樞紐元。例如,輸入為8,1,4,9,0,3,5,2,7,6,它的左邊元素為8,右邊元素為6,中間位置(left+right)/2的元素為0,於是樞紐元為這三個元素的中值,即6。使用三數中值分割法消除了預排序輸入的壞情形,並且實際減少了14%的比較次數。

分割策略

快速排序如歸併排序一樣,都採用分治的遞迴演算法。在快排的分割階段要做的就是把所有小元素移到陣列的左邊而把大元素移到陣列的右邊,當然,“小”和“大”是相對於樞紐元而言的。

由於我們採用三數中值分割法來選取樞紐元,對於一個數組,我們在a[left]、a[right]和a[center]中選取中值作為樞紐元,並將三者中最小的移至a[left],最大的移至a[right],這樣子有額外的好處,因為a[left]本來就是在分割階段需要放置比樞紐元小的元素,a[right]也是如此。之後我們需要將樞紐元與a[right-1]位置的元素交換位置,並將i和j初始化為left+1和right-2。

初始化完成之後就可以進行移動並與樞紐元進行比較,i向右移動,當遇到比樞紐元大的元素時停止,然後向左移動j,當j遇到比樞紐元小的元素時停止,此刻比較i和j的位置,如果i仍在j的左邊,則交換i、j位置處的元素。交換完後i與j繼續移動,直到i移動到j的右邊時停止兩者的移動。並將i停止移動時位置上的元素與樞紐元(a[right-1]位置)交換。此時樞紐元左邊的所有元素都小於樞紐元,右邊的所有元素都大於樞紐元。

分割過程解析

假設現有這樣一個待排序陣列:8 1 4 9 0 3 5 2 7 6。我們首先使用三數中值分割法選取出樞紐元,並將三元素中的最小者0移至a[left],最大者8移至a[right],以及作為中值的樞紐元6移至a[right-1] 將i和j初始化為left+1和right-2,比較a[left+1]的元素1,小於樞紐元,右移i,發現下一個元素4仍然小於樞紐元,繼續右移,9大於樞紐元,停止移動i。開始對j進行移動,但是我們發現a[right-2]的元素2小於樞紐元,於是停止對j的移動。此時i在j的左邊,於是交換i和j位置上的元素。 交換完後繼續從i開始移動,但是很遺憾下一個元素7大於樞紐元,i只好停止移動,j遇到的下一個元素5小於樞紐元,停止j,交換i和j的位置 繼續移動i和j,這時i停在7位置處,j停在3位置處,我們發現i此時跑到了j的右邊,於是這一輪i和j的移動結束,將i位置處的元素7與樞紐元6交換位置,此時我們的分割策略就結束了,可以發現,樞紐元6的左邊所有元素均小於它,右邊的元素都大於樞紐元 通過一次移動我們將整個陣列分成了兩個集合,接下來我們只需要在兩個集合中繼續使用快排,最終就能獲得一段有序的序列。

程式碼解析

/**
 * @author: zhangocean
 * @Date: 2018/11/11 14:49
 */
public class QuickSort {

    /**
     * 三數中指分割法
     */
    private int median3(int[] arr, int left, int right){
        int mid = (left+right)/2;
        if(arr[left] > arr[mid]){
            swapReferences(arr, left, mid);
        }
        if(arr[left] > arr[right]){
            swapReferences(arr, left, right);
        }
        if (arr[mid] > arr[right]){
            swapReferences(arr, mid, right);
        }
        swapReferences(arr, mid, right-1);
        return arr[right-1];
    }

    /**
     * 快排核心程式碼
     */
    private void quickSort(int[] arr, int left, int right) {
        if(left < right){
            //選出樞紐元,並將樞紐元放置在right-1位置處
            int pivot = median3(arr, left, right);
            int i = left+1;
            int j = right-2;
            if(i <= j){
                for(;;){
					//當i或j位置處的元素等於樞紐元時,也需要停止i或j的移動
                    while (arr[i] < pivot) {
                        i++;
                    }
                    while (arr[j] > pivot){
                        j--;
                    }
                    if(i < j){
                        swapReferences(arr, i, j);
                    } else {
                        break;
                    }
                }
                swapReferences(arr, i, right-1);
                quickSort(arr, left, i-1);
                quickSort(arr, i+1, right);
            }

        }
    }

    /**
     * 交換元素
     */
    private void swapReferences(int[] arr, int left, int right){
        int temp = arr[left];
        arr[left] = arr[right];
        arr[right] = temp;
    }

    public static void main(String[] args) {
        int[] arr = new int[10];
        Random random = new Random();
        for(int i=0;i<10;i++){
            arr[i] = random.nextInt(10);
        }
        System.out.println("排序前:" + Arrays.toString(arr));
        QuickSort sort = new QuickSort();
        sort.quickSort(arr, 0, arr.length-1);
        System.out.println("排序後:" + Arrays.toString(arr));
    }
}

輸出結果:

排序前:[76, 19, 27, 81, 80, 82, 23, 86, 94, 16]
排序後:[16, 19, 23, 27, 76, 80, 81, 82, 86, 94]

時間複雜度:O(N²) 空間複雜度:O(NlogN)

程式碼梳理

對於快排演算法的程式碼比較複雜,首先median3(int[],int,int)方法為三數中值分割法,在該方法中選取出了樞紐元,並將樞紐元放置在a[right-1]位置處,然後返回樞紐元以便後面移動中的比較。

quickSort(int[],int,int)為快排的核心程式碼,在程式碼的32、33行處將i和j分別初始化在left+1和right-2位置處。程式碼36-41行則是對i和j進行移動操作,當i和j停止移動時,會在42行處判斷i和j的相對位置,如果i沒有跑到j的右邊,則交換它倆此刻位置上的元素,否則,結束此輪i和j的移動。

程式碼48-50行則是將i最後停止的位置與樞紐元進行交換,並在樞紐元的左右集合中繼續遞迴的使用快排。為了避免初始化時將i初始化到j的右邊,這裡我在34行處對i和j的初始位置進行判斷,當遞迴集合中的元素小於等於3個時,就不要i和j的元素移動了,因為在三數中值分割法中就已經排好這三個元素的位置了。

總結

從程式碼上我們也可以很清楚的看到氣泡排序對於快排的簡單性,但是快排在大多數情況下對於運算時間有極大的優化。快速排序的最壞時間複雜度達到了O(N²),但是在平均情況下它的時間複雜度為O(NlogN)。對於排序演算法的選擇也是有技巧的,我也會在後面的部落格中對每一種排序演算法進行總的比較。