排序(二)時間複雜度爲O(nlogn)的排序算法

時間複雜度爲O(nlogn)的排序算法(歸併排序、快速排序),比時間複雜度O(n²)的排序算法更適合大規模數據排序。java

歸併排序

歸併排序的核心思想

採用「分治思想」,將要排序的數組從中間分紅先後兩個部分,而後對先後兩個部分分別進行排序,再將排序好的兩部分合並在一塊兒,這樣數組就有序了。算法

分治是一種解決問題的思想,遞歸是一種編程技巧,使用遞歸的技巧就是,先找到遞歸公式和終止條件,而後將遞歸公式翻譯成遞歸代碼。編程

歸併排序的遞推公式和終止條件:

//遞歸公式
merge_sort(p...r) = mege(merge_sort(p...q),merge_sort(q+1,r));

//終止條件
p >= r,再也不繼續分解

歸併排序代碼

public class MergeSort {

    public static void main(String[] args) {
        int[] a = {4, 3, 2, 1, 6, 5};
        mergeSort(a,0,a.length - 1);
        for (int i : a) {
            System.out.println(i);
        }
    }

    public static void mergeSort(int[] a, int p, int r) {
        //終止條件
        if (p >= r) return;

        int q = (r - p) / 2 + p;
        //遞歸公式
        mergeSort(a, 0, q);
        mergeSort(a, q + 1, r);

        //到這裏遞歸結束,能夠假設[0,q],[q + 1,r]已經排好序了
        merge(a, p, q, r);

    }

    private static void merge(int[] a, int p, int q, int r) {
        int i = p;
        int j = q + 1;
        int k = 0;
        int[] temp = new int[r - p + 1];
        while (i <= q && j <= r) {
            if (a[i] <= a[j]) {
                temp[k++] = a[i++];
            } else {
                temp[k++] = a[j++];
            }
        }

        //判斷哪一個子數字中有數據,判斷依據必須是 <=
        int start = i;
        int end = q;
        if (j <= r) {
            start = j;
            end = r;
        }

        //將剩餘數據拷貝到臨時數組temp中
        while (start <= end) {
            temp[k++] = a[start++];
        }

        //將temp數組中數據[0,r-p],拷貝至a數組中原來位置
        //能夠直接使用數組複製函數
        for (int n = 0; n <= r - p; n++) {
            a[p + n] = temp[n];
        }
    }
}

優化

能夠利用哨兵節點對merge方法進行優化,將數組分配兩部分,並將Integer.MAX_VALUE添加到每一個數組的最後一位,就能夠一次性將兩個數組中數據所有比較完,不會剩餘數據數組

//優化merge代碼
private static void mergeBySentry(int[] a, int p, int q, int r) {
    int[] leftArr = new int[q - p + 2];
    int[] rightArr = new int[r - q + 1];

    for (int i = 0; i <= q - p; i++) {
        leftArr[i] = a[p + i];
    }
    leftArr[q - p + 1] = Integer.MAX_VALUE;

    for (int i = 0; i < r - q; i++) {
        rightArr[i] = a[q + i + 1];
    }
    rightArr[r - q] = Integer.MAX_VALUE;


    int i = 0;
    int j = 0;
    int k = p;
    while (k <= r) {
        if (leftArr[i] <= rightArr[j]) {
            a[k++] = leftArr[i++];
        } else {
            a[k++] = rightArr[j++];
        }
    }
}

穩定性

歸併排序是穩定的排序算法,是否穩定取決於合併merge方法,當兩個數組有相同數據合併時,能夠先將左邊的數據先存入temp中,這樣就能夠保證穩定性函數

時間複雜度

最好狀況、最壞狀況,仍是平均狀況,時間複雜度都是 O(nlogn)。推導過程待補充...性能

空間複雜度

歸併排序不是原地排序算法。優化

遞歸代碼的空間複雜度並不能像時間複雜度那樣累加。剛剛咱們忘記了最重要的一點,那就是,儘管每次合併操做都須要申請額外的內存空間,但在合併完成以後,臨時開闢的內存空間就被釋放掉了。在任意時刻,CPU 只會有一個函數在執行,也就只會有一個臨時的內存空間在使用。臨時內存空間最大也不會超過 n 個數據的大小,因此空間複雜度是 O(n)ui

快速排序

快速排序核心思想

對數組p到r進行排序,從數組中從中取出一個數據做爲pivot(分區點),將小於pivot的放在左邊,大於pivot的放在右邊,以後利用分治、遞歸思想,再對左右兩邊的數據進行排序,直到區間縮小爲1,說明數據有序了翻譯

遞歸公式和終止條件

//遞歸公式
quick_sort(p...r) = quick_sort(p...q) + quick_sort(q + 1 ... r)  
    
//終止條件
p >= r

快速排序代碼

public static void quickSort(int[] a, int n){
    quickSortInternally(a,0,n - 1);
}

private static void quickSortInternally(int[] a,int p, int r){
    if (p >= r) return;

    int q = partition(a, p, r);
    quickSortInternally(a,p,q - 1);
    quickSortInternally(a,q + 1,r);
}

//p:起始位置,r:終止位置
private static int partition(int[] a, int p, int r) {
    //取出中間點
    int pivot = a[r];

    //i、j爲雙指針,i始終指向大於中間點的第一個元素,j不斷遍歷數組,最終指向最後一個元素即中間點
    int i = p;
    //比較從p開始,到r-1結束
    for(int j = p; j < r; ++j) {
        //若是小於中間點
        if (a[j] < pivot) {
            if (i == j) {
                //若是i和j相等,說明以前沒有大於中間點的元素,i和j都加1
                // j在進行下一輪循環的時候會自動加1,因此在這裏只加i
                ++i;
            } else {
                //若是不相等,說明i已經指向第一個大於中間點的元素
                // 須要將小於中間的的a[j]與a[i]交換位置,而後都加1
                int tmp = a[i];
                a[i++] = a[j];
                a[j] = tmp;
            }
        }
    }

    //循環結束,i指向大於中間點a[r]的第一個元素
    //將a[i]與a[r]交換位置
    int tmp = a[i];
    a[i] = a[r];
    a[r] = tmp;

    System.out.println("i=" + i);
    //返回交換後中間點座標位置
    return i;
}

性能分析

快速排序是原地、不穩定的排序算法,時間複雜度在大部分狀況下的時間複雜度均可以作到 O(nlogn),只有在極端狀況下,纔會退化到 O(n²)指針

原地:空間複雜度爲O(1),不須要佔用額外存儲空間

不穩定:由於分區的過程涉及交換操做,若是數組中有兩個相同的元素,好比序列 6,8,7,6,3,5,9,4,在通過第一次分區操做以後,兩個 6 的相對前後順序就會改變。因此,快速排序並非一個穩定的排序算法

時間複雜度:待補充

思考

O(n) 時間複雜度內求無序數組中的第 K 大元素。好比,4, 2, 5, 12, 3 這樣一組數據,第 3 大元素就是 4。

思路:

  • 選擇數組A[0,n-1]的最後一個元素A[n-1]做爲中間點pivot

  • 對數組A[0,n-1]原地分區,分爲[0,p-1],[p],[p+1,n-1],此時[0,p-1]這個分區中雖然可能無序,可是所有是比中間點小的元素,因此[p]爲這羣數中的第p+1大元素(下標爲p,因此共有p+1個元素,應該是p+1大)

  • 比較p+1和K,若是p+1 = K,說明A[p]就是求解元素,若是K > p+1,說明求解元素出如今A[p+1,n-1]中,則按照上面方法遞歸對A[p+1,n-1]進行分去查找,同理,若是K < p+1,則對A[0,p-1]進行分區查找

時間複雜度:O(n)。第一次分區查找,咱們須要對大小爲 n 的數組執行分區操做,須要遍歷 n 個元素。第二次分區查找,咱們只須要對大小爲 n/2 的數組執行分區操做,須要遍歷 n/2 個元素。依次類推,分區遍歷元素的個數分別爲、n/二、n/四、n/八、n/16.……直到區間縮小爲 1。若是咱們把每次分區遍歷的元素個數加起來,就是:n+n/2+n/4+n/8+…+1。這是一個等比數列求和,最後的和等於 2n-1。因此,上述解決思路的時間複雜度就爲 O(n)。

笨方法:每次取數組中的最小值,將其移動到數組的最前面,而後在剩下的數組中繼續找最小值,以此類推,執行 K 次,也能夠找到第K大元素。但這種方法的時間複雜度爲O(K*n),在K值比較小時,時間複雜度爲O(n),當K爲n/2或n時,時間複雜度就爲O(n²)了

思考2

如今你有 10 個接口訪問日誌文件,每一個日誌文件大小約 300MB,每一個文件裏的日誌都是按照時間戳從小到大排序的。你但願將這 10 個較小的日誌文件,合併爲 1 個日誌文件,合併以後的日誌仍然按照時間戳從小到大排列。若是處理上述排序任務的機器內存只有 1GB,你有什麼好的解決思路,能「快速」地將這 10 個日誌文件合併嗎

相關文章
相關標籤/搜索