1. 程式人生 > >資料結構與演算法之美專欄學習筆記-排序(下)

資料結構與演算法之美專欄學習筆記-排序(下)

分治思想

分治思想

分治,顧明思意就是分而治之,將一個大問題分解成小的子問題來解決,小的子問題解決了,大問題也就解決了。

分治與遞迴的區別

分治演算法一般都用遞迴來實現的。分治是一種解決問題的處理思想,遞迴是一種程式設計技巧。

 

歸併排序

演算法原理

歸併的思想

先把陣列從中間分成前後兩部分,然後對前後兩部分分別進行排序,

再將排序好的兩部分合併到一起,這樣整個陣列就有序了。

這就是歸併排序的核心思想。如何用遞迴實現歸併排序呢?

寫遞迴程式碼的技巧就是分寫得出遞推公式,然後找到終止條件,最後將遞推公式翻譯成遞迴程式碼。

 

遞推公式

merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))

終止條件

p >= r 不用再繼續分解

程式碼實現

public static void MergeSort(int[] data,int n){
    //傳入陣列、索引0和最後一位的索引
    Merge_c(data, 0, n - 1);
}
public static void Merge_c(int[] data,int p,int r){
    //遞迴終止條件:p與r相等或p大於r即細分到每個資料成員
    if (p >= r) return;
    //定義q為中間值
    int q = (p + r) / 2;
    //對q和中間值、中間值和r繼續細分
Merge_c(data,p,q); Merge_c(data,q+1,r); //直到細分到每個資料成員返回後,開始兩兩合併 Merge(data,p,q,r); } public static void Merge(int[] data,int front,int mid,int back){ //定義data陣列中第front到mid的陣列組成的陣列 int[] frontArray = new int[mid + 1]; for (int n = front; n < frontArray.Length; n++) frontArray[n]
= data[n]; //定義data陣列中第mid到back的陣列組成的陣列 int[] backArray = new int[back - mid]; for (int n = mid; n < backArray.Length; n++) backArray[n] = data[n]; //定義臨時陣列,長度為陣列中第front到back間的資料的長度 int[] temp = new int[back - front + 1]; //定義三個臨時變數作為遊標,分別初始化為front和mid+1,以及臨時陣列中的最後一個數據的位置為0 int i = front, j = mid+1, k = 0; //迴圈直到i超過了mid或者j超過了back while(i<=mid&&j<=back){ //根據大小,將data的第i/j的資料存入temp陣列 if (data[i] < data[j]) temp[k++] = data[i++]; else temp[k++] = data[j++]; } //定義兩個臨時變數為記錄起始位置,初始化為合併的兩陣列中的前一個數組的頭尾索引 int start = i, end = mid; //如果是後一個數組沒有遍歷完,就改為後一個數組的頭尾索引 if (j <= back){ start = j; end = back; } //將未遍完的陣列剩餘的資料存入temp陣列 while (start <= end) temp[k++] = data[start++]; //將完成排序的temp數組合併到對應的data陣列位置 for (int l = 0; l < temp.Length; l++) data[front + l] = temp[l]; }

效能分析

演算法穩定性

歸併排序是一種穩定排序演算法。

時間複雜度

歸併排序的時間複雜度是O(nlogn)。

空間複雜度

歸併排序演算法不是原地排序演算法,空間複雜度是O(n)

因為歸併排序的合併函式,在合併兩個陣列為一個有序陣列時,需要藉助額外的儲存空間

 

快速排序

演算法原理

快排的思想

如果要排序陣列中下標從p到r之間的一組資料,我們選擇p到r之間的任意一個數據作為pivot(分割槽點)。

然後遍歷p到r之間的資料,將小於pivot的放到左邊,將大於pivot的放到右邊,將povit放到中間。

經過這一步之後,陣列p到r之間的資料就分成了3部分,前面p到q-1之間都是小於povit的,中間是povit,後面的q+1到r之間是大於povit的。

根據分治、遞迴的處理思想,我們可以用遞迴排序下標從p到q-1之間的資料和下標從q+1到r之間的資料,直到區間縮小為1,就說明所有的資料都有序了。

 

遞推公式

quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)

終止條件

p >= r

程式碼實現

專欄寫的快排拆成三個方法讓人頭疼,我用C#改寫了群裡演算法大佬用c寫的快排,簡單明瞭。

public static void QuickSort(int[] data,int front,int back)
{
    //定義頭尾索引、分割槽點
    int i = front, j = back, mid = data[(front + back) / 2];
    //迴圈到i大於j
    while (i <= j)
    {
        //從頭索引i開始遍歷陣列,直到找到比分割槽點大的陣列成員
        while (data[i] < mid) i++;
        //從尾索引j開始遍歷陣列,直到找到比分割槽點小的陣列成員
        while (data[j] > mid) j--;
        //如果i、j都找到了,此時i仍然比j小,而且前者比後者大,就交換二者位置,使兩個陣列成員有序
        if (i <= j)
        {
            int temp = data[i];
            data[i] = data[j];
            data[j] = temp;
            //交換完成後,兩索引步進直到i比j大結束迴圈
            i++;j--;
        }
    }
    //迴圈結束後如果i仍然小於尾或者j仍然大於頭,根據前面的條件此時i肯定是大於j的
    //就以原頭為新頭,j為新尾,i為新頭,原尾為新尾,遞迴自身呼叫,遞迴到終點後陣列必定有序
    if (i < back) QuickSort(data,i, back);
    if (front < j) QuickSort(data,front, j);
}

效能分析

演算法穩定性

快速排序是不穩定的排序演算法。

時間複雜度

如果每次分割槽操作都能正好把陣列分成大小接近相等的兩個小區間,

那快排的時間複雜度遞推求解公式跟歸併的相同。快排的時間複雜度也是O(nlogn)。

如果陣列中的元素原來已經有序了,快排的時間複雜度就是O(n^2)。

前面兩種情況,一個是分割槽及其均衡,一個是分割槽極不均衡,

它們分別對應了快排的最好情況時間複雜度和最壞情況時間複雜度。

T(n)大部分情況下是O(nlogn),只有在極端情況下才是退化到O(n^2)。

空間複雜度

快排是一種原地排序演算法,空間複雜度是O(1)

 

歸併排序與快速排序的區別

歸併排序

先遞迴呼叫,再進行合併,合併的時候進行資料的交換。所以它是自下而上的排序方式。

何為自下而上?就是先解決子問題,再解決父問題。
快速排序

先分割槽,在遞迴呼叫,分割槽的時候進行資料的交換。所以它是自上而下的排序方式。

何為自上而下?就是先解決父問題,再解決子問題。

思考

O(n)時間複雜度內求無序陣列中第K大元素


有10個訪問日誌檔案,每個日誌檔案大小約為300MB,每個檔案裡的日誌都是按照時間戳從小到大排序的。現在需要將這10個較小的日誌檔案合併為1個日誌檔案,合併之後的日誌仍然按照時間戳從小到大排列。如果處理上述任務的機器記憶體只有1GB,你有什麼好的解決思路能快速地將這10個日誌檔案合併