分治法中的合併排序和快速排序

分治法

這裏寫圖片描述

分治法的步驟:

  • 分:將問題分解爲同一類型規模更小且最好相同的子問題;
  • 治:對這些子問題求解(一般是遞歸方法);
  • 合:將已解決的子問題合併,最終得出“母”問題的解。如果不合並,可以考慮用貪心法或動態規劃法。

在劃分階段治:快速排序
在合併階段治:合併排序

時間複雜度

算法運行時間的遞推公式:T(n)=aT(n/b)+f(n)

一個規模爲n的實例可以劃分爲b個規模爲n/b的實例,其中a個實例是需要求解的。f(n)是一個函數,表示將問題分解成小問題和將結果合併起來所消耗的時間。(比如:a=b=2,f(n)=1)

得到以下通用的時間複雜度公式:
這裏寫圖片描述

具體解析參考:


合併排序

僞代碼:

mergesort(A[0,n-1],first,last)
//遞歸調用mergesort對數組A[0,n-1]進行排序
//輸入:無序數組A[0,n-1],first數組起點,last數組終點
//輸出:非降序數組A[0,n-1]
int mid = (first + last)/2 //初始時mid = (0 + n-1)/2
mergesort(A[],0,mid) //①左子序
mergesort(A[],mid+1,n-1) //①右子序
merge(A[],first,last) //②合併
  • ①: 分
  • ② :治、合(即算法的主要工作在合併子問題的解

時間複雜度

遞推公式:n>1時,T(n)=2T(n/2)+f(n);T(1)=0
其中最壞情況下f(n)=n-1,得到:
n>1時,Tw (n)=2T(n/2)+n-1;Tw (1)=0

  • 精確解:Tw (n)=nlog2 n-n+1
  • 主定理:Θ(nlogn)
  • 穩定性:穩定

JAVA代碼:

注意這裏的空間複雜度是:n
有的書上是在merge()合併有序數列時分配臨時數組,但是過多的new操作會非常費時。因此作了下小小的變化。只在test()中new一個臨時數組,後面的操作都共用這一個臨時數組。

public class Main {

    public static void main(String[] args) {
        test();
    }

    /**
     * 遞歸合併排序
     * @param array
     * @param first
     * @param last
     * @param temp 輔助數組
     */
    public static void mergesort(int array[], int first, int last, int[] temp) {
        if(first < last){
            int mid = (first + last)/2;
            mergesort(array, first, mid, temp);//左子列排序
            mergesort(array, mid +1, last, temp);//右子列排序
            merge(array, first, last, mid, temp);//二路合併
        }
    }

    /**
     * 將兩個有序數組合併爲一個有序數組
     * @param array
     * @param first
     * @param last
     * @param mid
     * @param temp 輔助數組
     */
    public static void merge(int[] array, int first, int last, int mid, int[]temp) {
        int p = first;
        int q = mid + 1;
        int n = mid;
        int m = last;
        int k = 0;

        while(p<=n && q<=m){
            if(array[p] <= array[q]){
                temp[k++] = array[p++];
            }else {
                temp[k++] = array[q++];
            }
        }
        while(p <= n){
            temp[k++] = array[p++];
        }
        while(q <= m){
            temp[k++] = array[q++];
        }
        //temp賦值到array中,此時k代表temp有多少個有效元素
        for(int i=0; i<k; i++){
            array[first + i] = temp[i];
        }
    }
    /**
     * 測試用例
     */
    public static void test() {
        //int[] array = {1,2,3,4,5};
        //int[] array = {5,4,3,2,1};
        int[] array = {8,5,3,1,1,7,2,5,9,4};
        int n = array.length;
        //注:如果輔助數組在merge中,則每次合併都要new出一個
        int[] temp = new int[n];
        mergesort(array, 0, n-1, temp);
        for(int i=0; i<n; i++){
            System.out.print(array[i] + " ");
        }
    }
}

快速排序

整體僞代碼:

quicksort(A[l,r])
//遞歸調用quicksort對數組A[l,r]進行快速排序
//輸入:數組A[0,n-1]的子數組A[l,r],由左右下標l和r定義
//輸出:非降序排列的子數組A[l,r]
if(r<l){
    s<——Partion(A[l,r],p)//①s是分裂的位置
    quicksort(A[l,s-1])//②
    quicksort(A[s+1,r])//②
}
  • ,同時在找可以分的點。如何找到分裂的位置是關鍵,並且一邊在找分裂位置,一邊在排序(算法的主要工作在與劃分階段,而不需要再去合併子問題的解了)
  • 隱含在數組中,不需要合
  • 我們這裏只選擇子數組的第一個元素作爲中軸點p

——>Partion()中是如何排序的呢?

不同的Partion()有不同的方法,這裏討論兩種快速排序的方法。

一、霍爾(Hoare)快速排序法

根據《算法設計與分析基礎》中的僞代碼。

僞代碼:

HoarePartion(A[l,r])
//以第一個元素作爲中軸
//輸入:數組A[0,n-1]的子數組A[l,r],由左右下標l和r定義
//輸出:A[l,r]的一個劃分,分裂點的位置作爲返回值
p<——A[l]
i<——l+1
j<——r
repeat
    repeat i<——i+1 until A[i]>=p
    repeat j<——j-1 until A[j]<=p
    swap(A[i],A[j])
until i>=j //分裂點條件
swap(A[i],A[j])//撤銷最後一次交換
swap(A[l],A[j])//分裂點元素交換,完成一次劃分
return j

——>思想:

分別從子數組的兩邊進行掃描(除中軸點p,從第二個元素開始左到右掃描用指針i表示;從右到左掃面用指針j表示),當遇到A[i]大於等於中軸的元素,且遇到A[j]小於等於中軸的元素,則暫停掃描。

——>爲什麼需要等於呢?

當遇到相同的元素時,可以是數組分的更平均,便於減小分治問題規模。

——>i的掃描會可能會越過子數組的邊界?

需要對i檢查下標越界的可能性。而j不會越界,因爲有中軸界限。

——>所有?

掃描暫停條件(注意與遞歸停止條件:l>r區別)是:A[i]大於等於中軸元素,A[j]小於等於中軸元素。接下來分爲三種情況處理:

  1. 當i < j時,即i和j還未相遇,交換A[i]和A[j],然後i+1、j-1後,繼續開始掃描。
  2. 當i > j時,即i和j已經相遇且交叉,一輪掃描結束,得到分裂點s=j(爲什麼不選i?此時A[i]比中軸元素大,A[j]比中軸元素小)。將A[j]與中軸交換以後,得到該數組的一個劃分。
  3. 當i = j時,即i和j已經相遇,兩邊都遇到相同的軸點p才停止的,p=A[i]=A[j],即分裂點s=i=j,也得到該數組的一個劃分。這裏就是掃描暫停條件等於的作用了。

這裏的2和3可以結合起來,只有i≥j,就交換中軸和A[j]的位置。——>遞歸停止條件,在代碼中表示跳出循環。

JAVA代碼:


public class Main {

    public static void main(String[] args) {
        test();
    }
    /**
     * 霍爾快速排序
     * @param A
     * @param l
     * @param r
     */
    public static void quicksort(int[] A,int l,int r) {
        if(l < r){
            int p = A[l];//軸點元素
            int i = l+1;
            int j = r;
            //不能寫成while(i<=j)。注意i=j的情況,1、1時失效。
            //因爲,i和j都指向第二個1,造成死循環。
            while(true){
                //i作爲指針從左到右掃描,且不能超過j
                while(A[i] < p){
                    i++;
                    if(i >= r){
                        break;
                    }
                }
                //j作爲指針從右到左掃描
                while(A[j] > p){
                    j--;
                }
                if(i < j){
                    swap(A, i, j);
                    i++;
                    j--;
                }else {
                    break;
                }
            }
            //分裂點條件
            if(i >= j){
                //j作爲分裂點,A[j]與軸點元素交換
                swap(A, l, j);
                quicksort(A, l, j-1);
                quicksort(A, j+1, r);
            }
        }

    }
    /**
     * 交換數組中的元素
     */
    public static void swap(int A[], int i, int j) {
        int temp = A[i];
        A[i] = A[j];
        A[j] = temp;
    }

    /**
     * 測試用例
     */
    public static void test() {
        //int[] array = {1,2,3,4,5};
        //int[] array = {5,4,3,2,1};
        //int[] array = {1,1};
        int[] array = {8,5,3,1,1,7,2,5,9,4};
        int n = array.length;
        quicksort(array, 0, n-1);
        for(int i=0; i<n; i++){
            System.out.print(array[i] + " ");
        }
    }
}

時間複雜度

  • i和j掃描交叉時,鍵值的比較次數:n-1+2=n+1
  • i=j時,鍵值的比較次數:n-1+1=n

遞推公式:n>1時,T(n)=2T(n/2)+f(n);T(1)=0

最好情況:f(n)=n,n>1時,Tb (n)=2T(n/2)+n-1;Tb (1)=0

  • 精確解:n=2k ,Tb (n)=2k T(1)+k2k =k2k =nlog2 n
  • 主定理:Θ(nlog2 n)
  • 穩定性:不穩定

最壞情況:就是已排好的升序。
Tw (n)=(n+1)+n+…+3=(n+1)(n+2)/2-3=Θ(n2 )

平均情況:
Ta (n)=1.39nlog2 n

二、通常的快速排序

僞代碼:

 1 quicksort(A, lo, hi)
 2   if lo < hi
 3     p = partition(A, lo, hi)
 4     quicksort(A, lo, p - 1)
 5     quicksort(A, p + 1, hi)
 6 
 7 partition(A, lo, hi)
 8     pivot = A[hi]
 9     i = lo //place for swapping
10     for j = lo to hi - 1
11         if A[j] <= pivot
12             swap A[i] with A[j]
13             i = i + 1
14     swap A[i] with A[hi]
15     return i

——>思想:

首先選擇表頭作爲中間元素temp。然後,從j開始掃描,遇到小於temp的停止掃描,將A[i](此時的i在中間元素位置,並保存在temp中)與A[j]交換,然後i++。接着,從i開始掃描,遇到大於temp的停止掃描,將A[j]與A[i]交換,然後j- -。以此類推,直到i與j交叉或相遇,將temp賦值到A[i]中。

JAVA代碼:

public static void quicksort_general(int[] A,int l,int r){
         if (l < r)  
            {   
                int i = l;
                int j = r;
                int p = A[l];  
                while (i < j)  
                {  
                    //i<j爲越界限制
                    while(i < j && A[j] >= p) // 從右向左找第一個小於x的數  
                        j--;    
                    if(i < j)   
                        A[i++] = A[j];  

                    while(i < j && A[i] < p) // 從左向右找第一個大於等於x的數  
                        i++;    
                    if(i < j)   
                        A[j--] = A[i];  
                }  
                A[i] = p;  
                quicksort_general(A, l, i-1);
                quicksort_general(A, i+1, r);
            }
    }

參考資料:

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