1. 程式人生 > >演算法圖解之快速排序

演算法圖解之快速排序

分而治之(又稱D&C)

書中舉了一個例子,假設你是農場主,有一塊土地,如圖所示:

 

你要將這塊地均勻分成方塊,且分出的方塊要儘可能大。

 

 

從圖上看,顯然是不符合預期結果的。

那麼如何將一塊地均勻分成方塊,並確保分出的方塊是最大的呢?使用D&C策略。

(1)D&C演算法是遞迴的;
(2)使用D&C解決問題的過程包括兩個步驟:
a.找出基線條件,這種條件必須儘可能簡單;
b.不斷將問題分解(或者說縮小規模),直到符合基線條件;

就如何保證分出的方塊是最大的呢?《演算法圖解》中的快速排序一章提到了歐幾里得演算法。

什麼是歐幾里得演算法?

歐幾里得演算法又稱輾轉相除法,是指用於計算兩個正整數a,b的最大公約數。
應用領域有數學和計算機兩個方面。

舉個程式碼例子說一下歐幾里得演算法:

package cn.pratice.simple;

public class Euclid {

    
    public static void main(String[] args) {
        int m = 63;
        int n = 18;
        int remainer = 0;
        while(n!=0) {
            remainer = m % n;
            m = n;
            n = remainer;
        }
        
        System.out.println(m);
    }
}

 

最終的結果是9,正好63和18的最大公因數也是9.
其中也體現著分而治之的思想。記住,分而治之並非可用於解決問題的演算法而是一種解決問題的思路。

再舉個例子說明,如圖所示:

 

需要將這些數字相加,並返回結果,使用迴圈很容易完成這種任務,以Java為例:

 

package cn.pratice.simple;

public class Euclid {

    
    public static void main(String[] args) {
        int []num = new int[] {2,4,6};
        int total = 0;
        for (int i = 0; i < num.length; i++) {
            total += num[i];
                    
        }
        System.out.println(total);
    }
}

快速排序

快速排序是一種常用的排序演算法,比選擇排序快的多。
程式碼示例如下(快速排序):

package cn.pratice.simple;

public class QuickSort {
    
    //宣告靜態的 getMiddle() 方法,該方法需要返回一個 int 型別的引數值,在該方法中傳入 3 個引數
    public static int getMiddle(int[] list,int low,int high) {
        
        int tmp = list[low];//陣列的第一個值作為中軸(分界點或關鍵資料)
        
        while(low<high) {
            
            while(low<high && list[high]>tmp) {
                high--;
            }
            
            list[low] = list[high];//比中軸小的記錄移到低端
            
            while(low<high&&list[low]<tmp) {
                low++;
            }
            
            list[high]=list[low];//比中軸大的記錄移到高階
        }
        
        list[low] = tmp;//中軸記錄到尾
        
        return low;
    }
    
    //建立靜態的 unckSort() 方法,在該方法中判斷 low 引數是否小於 high 引數,如果是則呼叫 getMiddle() 方法,將陣列一分為二,並且呼叫自身的方法進行遞迴排序
    public static void unckSort(int[] list,int low,int high) {
        
        if(low<high) {
            
            int middle = getMiddle(list,low,high);//將list陣列一分為二
            unckSort(list,low,middle-1);//對低字表進行遞迴排序
            unckSort(list,middle+1,high);//對高字表進行遞迴排序
        }
    }
    
    //宣告靜態的 quick() 方法,在該方法中判斷傳入的陣列是否為空,如果不為空,則呼叫 unckSort() 方法進行排序
    public static void quick(int[] str) {
        if(str.length>0) {
            //檢視陣列是否為空
            unckSort(str,0,str.length-1);
        }
    }
    
    //測試
    public static void main(String[] args) {
        
        int[] number = {13,15,24,99,14,11,1,2,3};
        System.out.println("排序前:");
        for (int i : number) {
            System.out.print(i+" ");
        }
        
        quick(number);
        
        System.out.println("\r排序後:");
        for (int i : number) {
            System.out.print(i+" ");
        }
    }
}

此示例來自Java陣列排序:Java快速排序(Quicksort)法

沒有什麼比程式碼示例來的直接痛快。

再談大O表示法

快速排序的獨特之處在於,其速度取決於選擇的基準值。

常見的大O執行時間圖,如下:

 

上述圖表中的時間是基於每秒執行10次操作計算得到的。這些資料並不準確,這裡提供它們只是想讓你對這些執行時間的差別有大致認識。實際上,計算機每秒執行的操作遠遠不止10次。 在該節中,作者說合並排序比選擇排序要快的多。合併排序,用數學公式表示為O(n log n),而選擇排序為O(n的2次方)。
合併程式碼排序例子如下:

package cn.pratice.simple;

import java.util.Arrays;

public class MergeSort {



    private static void mergeSort(int[] original) {
        if (original == null) {
            throw new NullPointerException("The array can not be null !!!");
        }
        int length = original.length;
        if (length > 1) {
            int middle = length / 2;
            int partitionA[] = Arrays.copyOfRange(original, 0, middle);// 拆分問題規模
            int partitionB[] = Arrays.copyOfRange(original, middle, length);
            // 遞迴呼叫
            mergeSort(partitionA);
            mergeSort(partitionB);
            sort(partitionA, partitionB, original);
        }
    }

    private static void sort(int[] partitionA, int[] partitionB, int[] original) {
        int i = 0;
        int j = 0;
        int k = 0;
        while (i < partitionA.length && j < partitionB.length) {
            if (partitionA[i] <= partitionB[j]) {
                original[k] = partitionA[i];
                i++;
            } else {
                original[k] = partitionB[j];
                j++;
            }
            k++;
        }
        if (i == partitionA.length) {
            while (k < original.length) {
                original[k] = partitionB[j];
                k++;
                j++;
            }
        } else if (j == partitionB.length) {
            while (k < original.length) {
                original[k] = partitionA[i];
                k++;
                i++;
            }
        }
    }

    private static void print(int[] array) {
        if (array == null) {
            throw new NullPointerException("The array can not be null !!!");
        }
        StringBuilder sb = new StringBuilder("[");
        for (int element : array) {
            sb.append(element + ", ");
        }
        sb.replace(sb.length() - 2, sb.length(), "]");
        System.out.println(sb.toString());
    }
    
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();    //獲取開始時間

        int original[] = new int[] { 13,15,24,99,14,11,1,2,3 };
        for (int i = 0; i < original.length; i++) {
            System.out.print(original[i]+" ");
        }
        mergeSort(original);
        print(original);
        long endTime = System.currentTimeMillis();    //獲取結束時間

        System.out.println("程式執行時間:" + (endTime - startTime) + "ms");    //輸出程式執行時間
        
    }
}

此示例來自
java實現合併排序演算法

比較快速排序與合併排序

還是以上面的程式碼例子為例:
快速排序程式碼例子,如下:

public static int getMiddle(int[] list,int low,int high) {
        
        int tmp = list[low];//陣列的第一個值作為中軸(分界點或關鍵資料)
        
        while(low<high) {
            
            while(low<high && list[high]>tmp) {
                high--;
            }
            
            list[low] = list[high];//比中軸小的記錄移到低端
            
            while(low<high&&list[low]<tmp) {
                low++;
            }
            
            list[high]=list[low];//比中軸大的記錄移到高階
        }
        
        list[low] = tmp;//中軸記錄到尾
        
        return low;
    }
    
    //建立靜態的 unckSort() 方法,在該方法中判斷 low 引數是否小於 high 引數,如果是則呼叫 getMiddle() 方法,將陣列一分為二,並且呼叫自身的方法進行遞迴排序
    public static void unckSort(int[] list,int low,int high) {
        
        if(low<high) {
            
            int middle = getMiddle(list,low,high);//將list陣列一分為二
            unckSort(list,low,middle-1);//對低字表進行遞迴排序
            unckSort(list,middle+1,high);//對高字表進行遞迴排序
        }
    }
    
    //宣告靜態的 quick() 方法,在該方法中判斷傳入的陣列是否為空,如果不為空,則呼叫 unckSort() 方法進行排序
    public static void quick(int[] str) {
        if(str.length>0) {
            //檢視陣列是否為空
            unckSort(str,0,str.length-1);
        }
    }
    
    //測試
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();    //獲取開始時間

        int[] number = { 13,15,24,99,14,11,1,2,3,2,32,4321,432,3,14,153,23,42,12,34,15,312,12,43,3214,43214,43214,43214,12,2432,12,34,24,4532,1234};

        quick(number);
        
    
        for (int i : number) {
            System.out.print(i+" ");
        }
        long endTime = System.currentTimeMillis();    //獲取結束時間

        System.out.println("程式執行時間:" + (endTime - startTime) + "ms");    //輸出程式執行時間
        
    }
}

輸出結果,如圖:

半天看不到輸出結果,而程式仍在執行中。如果將陣列中的元素還原為原來那幾個,則很快看到結果。

合併程式碼例子,如下:

package cn.pratice.simple;

import java.util.Arrays;

public class MergeSort {



    private static void mergeSort(int[] original) {
        if (original == null) {
            throw new NullPointerException("The array can not be null !!!");
        }
        int length = original.length;
        if (length > 1) {
            int middle = length / 2;
            int partitionA[] = Arrays.copyOfRange(original, 0, middle);// 拆分問題規模
            int partitionB[] = Arrays.copyOfRange(original, middle, length);
            // 遞迴呼叫
            mergeSort(partitionA);
            mergeSort(partitionB);
            sort(partitionA, partitionB, original);
        }
    }

    private static void sort(int[] partitionA, int[] partitionB, int[] original) {
        int i = 0;
        int j = 0;
        int k = 0;
        while (i < partitionA.length && j < partitionB.length) {
            if (partitionA[i] <= partitionB[j]) {
                original[k] = partitionA[i];
                i++;
            } else {
                original[k] = partitionB[j];
                j++;
            }
            k++;
        }
        if (i == partitionA.length) {
            while (k < original.length) {
                original[k] = partitionB[j];
                k++;
                j++;
            }
        } else if (j == partitionB.length) {
            while (k < original.length) {
                original[k] = partitionA[i];
                k++;
                i++;
            }
        }
    }

    private static void print(int[] array) {
        if (array == null) {
            throw new NullPointerException("The array can not be null !!!");
        }
        StringBuilder sb = new StringBuilder("[");
        for (int element : array) {
            sb.append(element + ", ");
        }
        sb.replace(sb.length() - 2, sb.length(), "]");
        System.out.println(sb.toString());
    }
    
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();    //獲取開始時間

        int original[] = new int[] { 13,15,24,99,14,11,1,2,3,2,32,4321,432,3,14,153,23,42,12,34,15,312,12,43,3214,43214,43214,43214,12,2432,12,34,24,4532,1234};
        for (int i = 0; i < original.length; i++) {
            System.out.print(original[i]+" ");
        }
        mergeSort(original);
        print(original);
        long endTime = System.currentTimeMillis();    //獲取結束時間

        System.out.println("程式執行時間:" + (endTime - startTime) + "ms");    //輸出程式執行時間
        
    }
}

輸出結果,如圖:

通過兩者對比,我們很容易得出合併排序比快速排序快。

參考這個合併排序和快速排序執行時間比較

作者通過實驗得出一個結論:當資料量較小的時候,快速排序比合並排序執行時間要短,執行時間短就表示快,但是當資料量大的時候,合併排序比快速排序執行時間要短。
由此通過我上述的程式碼實驗和該文章作者試驗,可證實這個結