各類排序算法及其優化

排序總覽

排序是是將一組“無序”的記錄序列調整爲“有序”的記錄序列的過程。若整個排序過程不需要訪問外存便能完成,則稱此類排序問題爲內部排序;反之,若參加排序的記錄數量很大,整個序列的排序過程不可能只在內存中完成,則稱此類排序問題爲外部排序。
排序的穩定性:若經過排序後,能使關鍵字相同的元素保持原記錄序列中相對位置不變,則稱排序算法是穩定的。
(注:穩定性並不能用來衡量一個排序算法的優劣,它只是對算法性質的一種描述。)
如非特別說明,本篇討論的排序算法都是內部排序算法,且排序後的有序序列爲從小到大的序列。
內部排序的過程是一個逐步擴大記錄的有序序列長度的過程。

基於不同的“擴大” 有序序列長度的方法,內部排序方法大致可分下列幾種類型:

插入類、交換類、選擇類、歸併類

一、插入類排序

將無序子序列中的一個或幾個記錄“插入”到有序序列中,從而增加記錄的有序子序列的長度。

直接插入排序

一趟直接插入排序的基本思想:

實現一趟插入排序基本有三個步驟:

  • 1、在R[1..i-1]中查找R[i]的插入位置,R[1..j]<R[i< R[j+1..i-1];
  • 2、將R[j+1..i-1]中的所有記錄均後移一個位置;
  • 3、將R[i] 插入(複製)到R[j+1]的位置上。

代碼實現:

public class Sort {
    public static void insertionSort(int a[], int n){
        int i, j;
        for(i = 1; i < n; i ++){
            int e = a[i];
            for(j = i - 1; j >= 0 && e < a[j]; j --){
                a[j + 1] = a[j];
            }
            a[j + 1] = e;
        }
    }
}

對於直接插入排序,最好的情況是已經有序,此時只需要進行n-1次比較,而不需要進行移動;而最壞的情況是記錄完全逆序,此時進行比較和移動的次數都是n^2級別的。故直接插入排序的時間複雜度爲O(n^2),且由其過程可以看到,直接插入排序算法是穩定的。

折半插入排序

觀察直接插入排序的過程我們發現,我們是把一個個未排序元素的往有序序列裏插入,這樣查找插入位置可以使用折半查找,最後再統一向後移動元素,於是有了折半插入排序:

public class Sort{
    public static void semiInsertionSort(int a[], int n){
        int i, j, low, high, mid, e;
        for(i = 1; i < n; i ++){
            e = a[i]; low = 0; high = i - 1;
            while(low <= high){
                mid = (low + high) / 2;
                if(e > a[mid])
                    low = mid + 1;
                if(e <= a[mid])
                    high = mid -1;
            }
            for(j = i - 1; j > high; j -- )
                a[j + 1] = a[j];
            a[high + 1] = e;
        }
}

折半插入排序一定程度上減少了元素比較的次數,但元素移動次數並沒改變,因此折半插入排序的時間複雜度仍然是O(n^2),它同樣是一種穩定的排序算法。

希爾排序

希爾排序的基本思想是,對待排記錄序列先作“宏觀”調整,再作“微觀”調整。將記錄序列分成若干子序列,分別對每個子序列進行插入排序。如將n個記錄分爲d個子序列:

其中,d 稱爲增量,它的值在排序過程中從大到小逐漸縮小,直至最後一趟排序減爲 1。這裏實現的希爾排序爲了邏輯清晰,使用的是每次除以2的增量序列,實際這個序列可以當做參數傳入,更好的增量序列可以有更好的效果。

public class Sort{
    public static void shellSort(int a[], int n){
        int i, j;
        for(int d = n/2; d >= 1; d = d/2)
            for(i = d; i < n; i ++){
                int e = a[i];
                for(j = i - d; j >= 0 && a[j] > e; j -= d){
                    a[j + d] = a[j];
                }
                a[j + d] = e;
            }
    }
}

希爾排序的時間複雜度分析比較複雜,但是可以嚴格證明,只要增量序列選取合適,希爾排序的平均時間複雜度是可以小於O(n^2)的,但仍然大於O(nlogn)。由於相同關鍵字可能被劃分到不同的子序列,故希爾排序是不穩定的。

二、交換類排序

交換排序通過“交換”無序序列中的記錄從而得到其中關鍵字最小或最大的記錄,並將它加入到有序子序列中,以此方法增加記錄的有序子序列的長度。

冒泡排序

冒泡排序的基本思想是,假設待排序序列長度爲n,從後往前依次兩兩比較相鄰元素,若逆序則交換它們,這樣的一輪比較稱爲一次冒泡,每輪冒泡其結果就是最小的元素浮現在第一位(水面),這也是冒泡排序名稱的由來。

public class Sort{
    public static void bubbleSort(int a[], int n){
        boolean flag;
        for(int i = 0; i < n; i ++) {
            flag = false;
            for(int j = n - 1; j >i; j --){
                if(a[j] < a[j - 1]) {
                    swap(a, j, j-1);
                    flag = true;
                }
            }
            if(flag == false) return;
        }

    }
    public static void swap(int[] arr, int i, int j) {
        //輔助函數,用於交換
        int t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }
}

顯然冒泡排序的時間複雜度是O(n^2),由於相鄰比較相等時並不會交換,故冒泡排序是一種穩定的排序。

快速排序

快速排序基於分治,其基本思想是找一個記錄,以它的關鍵字作爲“樞軸”,凡其關鍵字小於樞軸的記錄均移動至該記錄之前,反之,凡關鍵字大於樞軸的記錄均移動至該記錄之後。致使一趟排序(劃分)之後,記錄的無序序列R[s..t]將分割成兩部分:R[s..i-1]和R[i+1..t],且R[j]≤ R[i]≤ R[j] (s≤j≤i-1)樞軸(i+1≤j≤t),之後分別對分割所得兩個子序列遞歸進行劃分。

'''一般快速排序'''
public class Sort{    
    public static void quickSort(int a[], int low, int high){
        int i = low, j = high, temp;
        if(i < j){
            temp = a[low];//劃分樞軸
            while(i < j) {//避免順序情況出現下越界
                while (i < j && a[j] >= temp) j--;
                a[i] = a[j];
                while (i < j && a[i] <= temp) i++;
                a[j] = a[i];
            }
            a[i] = temp;
            quickSort(a, low, i - 1);
            quickSort(a, i + 1, high);
        }
    }
}

平均而言快速排序是一個O(nlogn)的算法,不過實際快速排序的時間複雜度與劃分是否對稱有關,因此樞軸的選取很重要,在上面給出的快速排序代碼中,每次使用的樞軸都是劃分後各序列的最左邊的元素,這就有一個問題,(最壞情況)如果原始序列是本就有序的或逆序的,這樣快速排序就退化爲一個O(n^2)的算法,並體現不出其快。爲了解決這個問題,我們每次選取樞軸時可以隨機從劃分後的序列中選取,而不是隻選擇最左邊那個:

'''應對幾乎有序或逆序情況的改進快速排序'''
public class Sort{    
    public static void quickSort(int a[], int low, int high){
        swap( a, low , (int)(Math.random()*(high-low+1)) + low );
        int i = low, j = high, temp;
        if(i < j){
            temp = a[low];//劃分樞軸
            while(i < j) {//避免順序情況出現下越界
                while (i < j && a[j] >= temp) j--;
                a[i] = a[j];
                while (i < j && a[i] <= temp) i++;
                a[j] = a[i];
            }
            a[i] = temp;
            quickSort(a, low, i - 1);
            quickSort(a, i + 1, high);
        }
    }
}

可以看到代碼只是加入了一行實現隨機選取序列中的元素與第一個元素進行交換,此時雖然仍有可能退化爲O(n^2)的算法,但是可以嚴格證明,當n趨於無窮時,這種情況發生的概率爲0,它是一個絕對平均的O(nlogn)算法。
還能不能繼續優化呢?其實對於所有的高級排序算法都有一個可用的優化,那就是當規模比較小時轉而使用插入排序,這樣往往會進一步提高排序效率,這是因爲當規模小到一定程度時小規模數據基本有序的概率大大增加,而插入排序對基本有序序列時間複雜度是接近O(n)的,所以給出快速排序的第三個優化:

'''小規模轉用插入排序'''
public class Sort{    
    public static void quickSort3(int a[], int low, int high){
        if(high - low <= 12){
            insertionSortH(a, low, high);
            return ;
        }
        swap( a, low , (int)(Math.random()*(high-low+1)) + low );
        int i = low, j = high, temp;
        if(i < j){
            temp = a[low];//劃分樞軸
            while(i < j) {//避免順序情況出現下越界
                while (i < j && a[j] >= temp) j--;
                a[i] = a[j];
                while (i < j && a[i] <= temp) i++;
                a[j] = a[i];
            }
            a[i] = temp;
            quickSort(a, low, i - 1);
            quickSort(a, i + 1, high);
        }
    }
    public static  void insertionSortH(int arr[], int l, int r){
        //插入排序l...r
        for( int i = l+1 ; i <= r ; i ++ ) {
            int e = arr[i];
            int j;
            for (j = i; j > l && arr[j-1] > e; j--)
                arr[j] = arr[j-1];
            arr[j] = e;
        }
        return;
    }
}

綜合來講快速排序算法的時間複雜度平均爲O(nlogn)。由於相同元素可能被劃分開與其它元素交換後相對位置改變,故快速排序是一種不穩定的排序。

三、選擇類排序

選擇類排序基本思想是從記錄的無序子序列中“選擇”關鍵字最小或最大的記錄,並將它加入到有序子序列中,以此方法增加記錄的有序子序列的長度。

簡單選擇排序

簡單選擇排序比較簡單,

直接給出代碼:

public class Sort{
   public static void selectionSort(int a[], int n){
        for(int i = 0; i< n -1; i ++){
            int minIndex = i;
            for(int j = i + 1; j < n; j ++)
                if(a[minIndex] > a[j])
                    minIndex = j;
            swap(a, i, minIndex);    
        }
    }
}

簡單選擇排序元素的比較次數和序列初始狀態無關,故時間複雜度始終爲O(n^2),且交換改變相對位置,故是一種不穩定的排序。

堆排序

在算法之美系列中,數據結構之優先隊列這篇文章詳細介紹了堆這種數據結構數據結構——堆,這裏不再贅述建堆和堆的維護過程,其排序的基本思想是建立一個最小堆,每次取出堆頂元素,並維護堆,這樣得到的序列就是有序的。堆排序的代碼:

public class Sort{
    public static void siftDown(int a[], int k, int n){
        while(2*k + 1 < n){//大於則是左孩子越界
            int j = 2*k + 1;
            if(j + 1 < n && a[j + 1] > a[j]){
                //右孩子大於左孩子
                j ++;//取右孩子
            }
            if(a[k] >= a[j]){
                break;
            }

            swap(a, k, j);
            k = j;

        }
    }
    public static void maxHeap(int a[], int n){
        //建堆過程
        for(int i = (n-1-1)/2; i  >= 0; i --)
            siftDown(a, i, n);
    }
    public static void heapSort(int a[], int n){
        //堆排序
        maxHeap(a, n);
        for(int i = n; i>0; i --){
            swap(a, 0, i - 1);

            siftDown(a, 0, i-1);
        }
    }
}

堆排序建堆的時間複雜度爲O(n),維護調整堆的時間複雜度爲O(logn)(因爲堆是完全二叉樹結構,其高度爲logn級別),故堆排序時間複雜度爲O(nlogn),且顯然是一種不穩定的排序。

四、歸併類排序

歸併排序的基本思想是通過合併兩個或兩個以上的記錄有序子序列,逐步增加記錄有序序列的長度,使之有序。(內部排序通常都是二路歸併。)

遞歸式歸併排序

public class Sort{
    public static void merge(int a[], int l, int m, int r){
        //歸併過程
        int b[] = new int[r - l + 1];
        for(int i =l; i <= r; i++)
            b[i-l] = a[i];
        int i = l, j = m+1,k;
        for(k = i; i <= m && j <= r; k++){
            if(b[i-l] < b[j-l]){
                a[k] = b[i ++ -l];   // a[k] = b[i -l];  i ++;
            }
            else{
                a[k] = b[j -l];
                j ++;
            }
        }
        while(i<=m) a[k++] = b[i++ -l];
        while(j<=r) a[k++] = b[j++ -l];

    }

    public static void mergeSort(int a[], int low, int high){
        //歸併排序
        if(low<high){
            int mid = (low+high) / 2;
            mergeSort(a, low, mid);
            mergeSort(a, mid+1, high);

            if(a[mid] > a[mid + 1])  //這裏是優化,去掉也可以執行,加
                //上的意思是若現在已經有序了,就不用比較了
                merge(a,low,mid, high);
        }
    }
}

迭代式歸併排序(非遞歸)

public class Sort{
    public static void mergeSortBU(int a[], int n){
        for(int sz = 1; sz <= n; sz += sz)
            for(int i = 0; i + sz < n; i += 2*sz)
                merge(a, i, i + sz - 1,min(i + 2 * sz - 1, n - 1));
    }
}

對於歸併排序,同樣可以加入當規模比較小時,使用插入排序以提高性能(實際上每個高級排序算法都可以有這個優化。)

全部代碼封裝爲排序類

將以上代碼封裝到一個類中,
排序類

'''Sort.java'''
import static java.lang.Integer.min;
public class Sort {
    public static void insertionSort(int a[], int n){
        int i, j;
        for(i = 1; i < n; i ++){
            int e = a[i];
            for(j = i - 1; j >= 0 && e < a[j]; j --){
                a[j + 1] = a[j];
            }
            a[j + 1] = e;
        }
    }

    public static void semiInsertionSort(int a[], int n){
        int i, j, low, high, mid, e;
        for(i = 1; i < n; i ++){
            e = a[i]; low = 0; high = i - 1;
            while(low <= high){
                mid = (low + high) / 2;
                if(e > a[mid])
                    low = mid + 1;
                if(e <= a[mid])
                    high = mid -1;
            }
            for(j = i - 1; j > high; j -- )
                a[j + 1] = a[j];
            a[high + 1] = e;
        }
    }

    public static void shellSort(int a[], int n){
        int i, j;
        for(int d = n/2; d >= 1; d = d/2)
            for(i = d; i < n; i ++){
                int e = a[i];
                for(j = i - d; j >= 0 && a[j] > e; j -= d){
                    a[j + d] = a[j];
                }
                a[j + d] = e;
            }
    }

    public static void bubbleSort(int a[], int n){
        boolean flag;
        for(int i = 0; i < n; i ++) {
            flag = false;
            for(int j = n - 1; j >i; j --){
                if(a[j] < a[j - 1]) {
                    swap(a, j, j-1);
                    flag = true;
                }
            }
            if(flag == false) return;
        }

    }

    public static void quickSort(int a[], int low, int high){
        int i = low, j = high, temp;
        if(i < j){
            temp = a[low];//劃分樞軸
            while(i < j) {//避免順序情況出現下越界
                while (i < j && a[j] >= temp) j--;
                a[i] = a[j];
                while (i < j && a[i] <= temp) i++;
                a[j] = a[i];
            }
            a[i] = temp;
            quickSort(a, low, i - 1);
            quickSort(a, i + 1, high);
        }
    }
    public static void quickSort2(int a[], int low, int high){
        swap( a, low , (int)(Math.random()*(high-low+1)) + low );
        int i = low, j = high, temp;
        if(i < j){
            temp = a[low];//劃分樞軸
            while(i < j) {//避免順序情況出現下越界
                while (i < j && a[j] >= temp) j--;
                a[i] = a[j];
                while (i < j && a[i] <= temp) i++;
                a[j] = a[i];
            }
            a[i] = temp;
            quickSort(a, low, i - 1);
            quickSort(a, i + 1, high);
        }

    }
    public static void quickSort3(int a[], int low, int high){
        if(high - low <= 12){
            insertionSortH(a, low, high);
            return ;
        }
        swap( a, low , (int)(Math.random()*(high-low+1)) + low );
        int i = low, j = high, temp;
        if(i < j){
            temp = a[low];//劃分樞軸
            while(i < j) {//避免順序情況出現下越界
                while (i < j && a[j] >= temp) j--;
                a[i] = a[j];
                while (i < j && a[i] <= temp) i++;
                a[j] = a[i];
            }
            a[i] = temp;
            quickSort(a, low, i - 1);
            quickSort(a, i + 1, high);
        }
    }
    public static void selectionSort(int a[], int n){
        for(int i = 0; i< n -1; i ++){
            int minIndex = i;
            for(int j = i + 1; j < n; j ++)
                if(a[minIndex] > a[j])
                    minIndex = j;
            swap(a, i, minIndex);
        }
    }

    public static void siftDown(int a[], int k, int n){
        while(2*k + 1 < n){//大於則是左孩子越界
            int j = 2*k + 1;
            if(j + 1 < n && a[j + 1] > a[j]){
                //右孩子大於左孩子
                j ++;//取右孩子
            }
            if(a[k] >= a[j]){
                break;
            }

            swap(a, k, j);
            k = j;

        }
    }
    public static void maxHeap(int a[], int n){
        //建堆過程
        for(int i = (n-1-1)/2; i  >= 0; i --)
            siftDown(a, i, n);
    }
    public static void heapSort(int a[], int n){
        //堆排序
        maxHeap(a, n);
        for(int i = n; i>0; i --){
            swap(a, 0, i - 1);

            siftDown(a, 0, i-1);
        }
    }
    public static void merge(int a[], int l, int m, int r){
        //歸併過程
        int b[] = new int[r - l + 1];
        for(int i =l; i <= r; i++)
            b[i-l] = a[i];
        int i = l, j = m+1,k;
        for(k = i; i <= m && j <= r; k++){
            if(b[i-l] < b[j-l]){
                a[k] = b[i ++ -l];   // a[k] = b[i -l];  i ++;
            }
            else{
                a[k] = b[j -l];
                j ++;
            }
        }
        while(i<=m) a[k++] = b[i++ -l];
        while(j<=r) a[k++] = b[j++ -l];

    }

    public static void mergeSort(int a[], int low, int high){
        //歸併排序
        if(low<high){
            int mid = (low+high) / 2;
            mergeSort(a, low, mid);
            mergeSort(a, mid+1, high);

            if(a[mid] > a[mid + 1])  //這裏是優化,去掉也可以執行,加
                //上的意思是若現在已經有序了,就不用比較了
                merge(a,low,mid, high);
        }
    }
    public static void mergeSortBU(int a[], int n){
        for(int sz = 1; sz <= n; sz += sz)
            for(int i = 0; i + sz < n; i += 2*sz)
                merge(a, i, i + sz - 1,min(i + 2 * sz - 1, n - 1));
    }
    public static void swap(int[] arr, int i, int j) {
        int t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }

    public static  void insertionSortH(int arr[], int l, int r){

        for( int i = l+1 ; i <= r ; i ++ ) {

            int e = arr[i];
            int j;
            for (j = i; j > l && arr[j-1] > e; j--)
                arr[j] = arr[j-1];
            arr[j] = e;
        }

        return;
    }

    public static void main(String args[]){
        int a[] = {3,7,4,6,5,9,1,-9,67};
        Sort.mergeSortBU(a, a.length - 1);
        for(int i = 0; i<a.length; i++)
            System.out.println(a[i]);
    }
}

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