1. 程式人生 > 其它 >two pointers思想與歸併排序

two pointers思想與歸併排序

以一個例子引入:給定一個遞增的正整數序列和一個正整數 M,求序列中的兩個不同位置的數 a 和 b,使得它們的和恰好為 M,輸出所有滿足條件的方案。 本題的一個最直觀的想法是,使用二重迴圈列舉序列中的整數 a 和 b,判斷它們的和是否為 M。時間複雜度為 O(n^2)。當n的規模足夠大時,這顯然是不可取的。

什麼是 two pointers

以一個例子引入:給定一個遞增的正整數序列和一個正整數 M,求序列中的兩個不同位置的數 a 和 b,使得它們的和恰好為 M,輸出所有滿足條件的方案。

  本題的一個最直觀的想法是,使用二重迴圈列舉序列中的整數 a 和 b,判斷它們的和是否為 M。時間複雜度為 O(n^2)。當n的規模足夠大時,這顯然是不可取的。

two pointers 將利用有序序列的列舉特性來有效降低複雜度。它針對本題的演算法如下:

令下標 i 的初值為0,下標 j 的初值為 n-1,即令 i、j 分別指向序列的第一個元素和最後一個元素,接下來根據 a[i]+a[j] 與 M 的大小來進行下面三種選擇,使 i 不斷向右移動、使 j 不斷向左移動,直到 i≥j 成立

  • 若 a[i]+a[j]==M ,說明找到了其中一種方案,令 i=i+1、j=j-1。

  • 若 a[i]+a[j]>M,令 j=j-1。

  • 若 a[i]+a[j]<M,令 i=i+1。

  反覆執行上面三個判斷,直到 i≥j 成立,在遞增序列的前提下,迴圈只需要進行到i>=j時停止,所以理想狀態下只需要遍歷半個序列,時間複雜度只需要O(n)

程式碼如下:

while(i < j) {
    if(a[i]+a[j] == M) {
        printf("%d %d\n", i, j);
        i++; j--;
    } else if(a[i]+a[j] < M) {
        i++;
    } else {
        j--;
    }
}

序列合併問題

再來看序列合併問題。假設有兩個遞增序列 A 與 B,要求將它們合併為一個遞增序列 C。

  同樣的,可以設定兩個下標 i 和 j ,初值均為0,表示分別指向序列 A 的第一個元素和序列 B 的第一個元素,然後根據 A[i] 與 B[j] 的大小來決定哪一個放入序列 C。

  • 若 A[i]≤B[j],把 A[i] 加入序列 C 中,並讓 i 加1
  • 若 A[i]>B[j],把 B[j] 加入序列 C 中,並讓 j 加1

上面的分支操作直到 i、j 中的一個到達序列末端為止,然後將另一個序列的所有元素依次加入序列 C 中,程式碼如下:

int merge(int A[], int B[], int C[], int n, int m) {
    int i=0, j=0, index=0;    // i指向A,j指向B,index指向C
    while(i<n && j<m) {
        if(A[i] <= B[j]) {            // 若 A[i]≤B[j]
            C[index++] = A[i++];
        } else {                    // 若 A[i]>B[j]
            C[index++] = B[j++];
        }
    }
    while(i<n)    C[index++] = A[i++];    // 若 A 有剩餘
    while(j<m)    C[index++] = B[j++];    // 若 B 有剩餘
    return index;        // 返回 C 長度
}

廣義上的 two pointers 是利用問題本身與序列的特性,使用兩個下標 i、j 對序列進行掃描(可以同向掃描,也可以反向掃描),以較低的複雜度(一般為 O(n) )解決問題。

歸併排序

歸併排序是一種基於“歸併”思想的排序方法,本節主要介紹其中最基本的 2-路歸併排序。2-路歸併排序的原理是,將序列兩兩分組,將序列歸併為\(\lceil \frac n 2 \rceil\)個組,組內單獨排序;然後將這些組再兩兩歸併,生成\(\lceil \frac n 4 \rceil\)個組,組內再單獨排序;以此類推,直到只剩下一個組為止。時間複雜度為 O(nlogn)。

說明:對於偶數個的陣列正常對半分就行,對於奇數個的陣列,留下最後1個多餘的單獨1組,其餘兩兩1組。

我們先寫一個merge函式將陣列的兩個區間合併為一個有序區間。

const int maxn = 100;
// 將陣列A的 [L1,R1] 與 [L2,R2] 合併為有序區間(此處 L2=R1+1 )
void merge(int A[], int L1, int R1, int L2, int R2) {
    int i=L1, j=L2;
    int temp[maxn], index=0;    // temp 臨時儲存合併序列
    while(i<=R1 && j<=R2) {
        if(A[i] <= A[j]) {            // 若 A[i] ≤ A[j]
            temp[index++] = A[i++];
        } else {                    // 若 A[i] > A[j]
            temp[index++] = A[j++];
        }
    }    
    while(i <= R1) temp[index++] = A[i++];
    while(j <= R2) temp[index++] = A[j++];
    for(int i=0; i<index; ++i) {
        A[L1+i] = temp[i];    // 將合併後的序列賦值回 A
    }
}

1. 遞迴實現

只需反覆將當前區間 [left,right] 分為兩半,對兩個子區間 [left,mid] 與 [mid+1, right] 分別遞迴進行歸併排序,然後將兩個已經有序的子區間合併為有序序列即可。程式碼如下:

// 歸併排序遞迴實現
// 只需反覆將當前區間 [left,right] 分為兩半,對兩個子區間 [left,mid] 與 [mid+1, right]
// 分別遞迴進行歸併排序,然後將兩個已經有序的子區間合併為有序序列即可。
void mergeSort(int A[], int left, int right) {
    if(left < right) {    // 當 left==right 時,只有一個元素,認定為有序
        int mid = (left+right)/2;
        mergeSort(A, left, mid);            // 分為左區間和右區間
        mergeSort(A, mid+1, right);
        merge(A, left, mid, mid+1, right);    // 將左區間和右區間合併
    }
}

2.非遞迴實現

非遞迴實現主要考慮到這樣一點:每次分組時組內元素個數上限都是2的冪次。於是就可以想到這樣的思路:令步長 step 的初值為2,然後將陣列中每 step 個元素作為一組,將其內部進行排序;再令 step 乘以2,重複上面的操作,直到 step/2 超過元素個數 n 。程式碼如下:

// 歸併排序非遞迴實現
// 令步長 step 的初值為2,然後將陣列中每 step 個元素作為一組,
// 將其內部進行排序;再令 step 乘以2,重複上面的操作,直到 step/2 超過元素個數 n 。
void mergeSort(int A[]) {
    // step 為組內元素個數
    for(int step=0; step/2 <= n; step *= 2) {
        for(int i = 1; i <= n; i += step) {    // 對每一組,陣列下標從1開始
            int mid = i + step/2 -1;    // 左區間元素個數為 step/2
            if(mid+1 <= n) {    // 右區間存在元素
                // 左區間為 [left,mid],右區間為 [mid+1, min(i+step-1,n)
                merge(A, i, mid, mid+1, min(i+step-1, n));
            }

            /*
            // 28-32行也可以用 sort 代替 merge 函式,只要時間允許
            sort(A+i, A+min(i+step, n+1));
            */
        }
        // 用 sort 代替,此處輸出歸併排序的某一趟結束時的序列
    }
}

如果題目只要求給出歸併排序每一趟結束時的序列,可以用sort函式代替 merge 函式,只要時間允許。

3.演算法分析

時間複雜度: O(nlogn)

歸併排序的執行效率與要排序的原始陣列的有序程度無關,所以其時間複雜度是非常穩定的,不管是最好情

況、最壞情況,還是平均情況,時間複雜度都是 O(nlogn)。

空間複雜度:O(n)

它有一個致命的“弱點”,那就是歸併排序不是原地排序演算法。

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

在每次進行合併操作時,需要O(n)的陣列空間存放資料。

參考自:https://www.cnblogs.com/coderJiebao/p/Algorithmofnotes08.html