算法導論第2章 分治法與歸併排序, 二分查找法

分治策略:將原問題劃分紅n個規模較小而結構與原問題類似的子問題,而後遞歸地解決這些子問題,最後再合併其結果,就能夠獲得原問題的解。算法

它須要三個步驟:編程

  1. 分解:將原問題分解成一系列的子問題。
  2. 解決:遞歸地解決各個子問題。若子問題足夠小,則直接求解
  3. 合併:將子問題的結果合併成原問題的解。

經過分治策略和分治步驟,能夠簡單地默出歸併算法。數組

  1. 分解:將n個元素分紅各自包含n/2個元素的子序列
  2. 解決:用歸併排序法遞歸地對兩個子序列進行排序。
  3. 合併:合併兩個以排序的子序列,獲得排序結果:

書上的變量q、p、r太難理解,改成了left, middle(m), right。分別表明數組的左起點,中間分隔點,和右終點。編程語言

void merge(int * a, int left, int m, int right) {
    int n1 = m - left;
    int n2 = right - m;
    int *L = new int[n1];
    int *R = new int[n2];
    
    memcpy(L, a+left, n1 * sizeof(int));
    memcpy(R, a+m, n2 * sizeof(int));
    /*
    for (int i = 0; i < n1; i++) {
        L[i] = a[left+i];
    }

    for (int j = 0; j < n2; j++) {
        R[j] = a[m+j];
    }
    */
    int i = 0;
    int j =0;
    for (int k = left; k < right; k++) {
        if ((j >=  n2) //R已被取光了
            || ((i< n1)&& (L[i] <= R[j]))) {
            a[k] = L[i++];
        } else { // if (i>= n1) || ((j < n2) && L[i] > R[j])))
            a[k] = R[j++];
        }
    }
    delete[]L;
    delete[]R;
}
void mergeSort(int* a, int left, int right) {
    if (right - left < 2 ) {
        return;
    }
    int m = left + (right - left)/ 2;//分解
    mergeSort(a, left, m);//遞歸地對兩個子序列進行排序
    mergeSort(a, m, right);
    merge(a, left, m, right);//合併
}

 

對於merge函數中的合併過程,有必要也用循環不變式來分析一下:函數

循環中不變的量是a[left...k) 中包含了L[0..n1), R[0..n2)中的k-left個最小元素,而且是排好序的spa

  1. 初始化:在for循環開始以前,k = left。所以子數組a[left..k)是空的。這個空數組包含了L和R中k-left個最小元素,也就是0個元素。此外,i,j都是0,所以L[i], R[j]都是各自所在數組中,還沒有被複制會數組A的最小元素。
  2. 保持:分兩種狀況:
    1. R數組爲空,或者L、R數組都不爲空,且L[i] <= R[j]:   L[i]就是爲被複制回數組a的最小元素。因爲a[left...k)包含了k-left個最小元素,而且已經排好序了.所以將L[i]賦值到a[k]後,子數組a[left..k+1)將包含k-left+1個最小元素。增長k的值,會爲下一輪跌倒從新創建循環不變式的值-----》a[left...k) 中包含了L[0..n1), R[0..n2)中的k-left個最小元素,而且是排好序的.
    2. L數組爲空,或者L、R數組都不爲空且L[i] > R[j]:   R[j]就是會被複制回數組a的最小元素。將R[j]複製到a[k]後,子數組a[left..k)將包含k-left+1個最小元素,而且是已經排好序的.
  3. 終止:  當k=right時,根據循環不變式,子數組a[left..k),也就是a[left, right)。已經包含了 L[0..n1], R[0..n2]中的k-left個最小元素, 也就是right - left 個最小元素,而且是排好序的。數組L和R合併起來,包含了n1 + n2 = right - lef個元素,即全部元素都被複制到了數組a中。

 邊界條件:code

  1.  因C語言沒法表達書中僞代碼中的無窮大哨兵。所以必須顯式地判斷L、R數組不爲空。
  2. 書中僞代碼數組下標是從1開始的,而C語言的下標是從0開始的。所以當left = 0, right=1時,計算m時會出現m=0+(1-0)/2 = 0的狀況.從而陷入死循環。所以mergeSort的退出條件不能是left >= right,  而必須是right - left < 2。即left與right中間只有一個元素是便可退出.
  3. 取中一般算法是m = (left+right)/2, 但由於編程語言的限制,若是left值很是大則m有可能會有溢出,因此改成left + (right - left) / 2。由於left + (right - left) / 2< right。因此只要right不溢出,m就不會溢出。

歸併算法的時間複雜度是O(nlgn). 因合併時使用了兩個臨時數組,所以空間複雜度是O(n)blog








一樣的,二分查找也是分治法的應用。應用分治步驟,能夠很容易地默出二分查找法:
int binSearch(int* a, int target,int left, int right) {
  if (right < left ) {
    return -1;
  }
  int m = 0;
  while (left < right) {
    m = left + (right - left) / 2;
    if (a[m] == target) {
    return m;
    } else if (a[m] < target) {
      left = m+1;
    } else {
      right = m;
    }
  }
  return -1;
}

 採用循環不變式分析一下。循環中不變的量是:若是target存在,則它必定在數組範圍[left,right)中.排序

  1. 初始化: 給出的是全量數組,所以成立.
  2. 保持:
    1. 查找到target,直接返回下標.
    2. 若是a[m] <target, 則a[left,m+1)中必然沒有target存在. 所以可將查找範圍縮小至a[m+1,right)
    3. 同理, 可將查找範圍縮小至a[left,m).
      由此,咱們每次都確保了target一定在咱們的查找範圍內,落在a[left,right)區域範圍內
  3. 終止: 若是沒有找到的待查區域是一定會至少減小1個長度。也就是說程序一定會正確的終止不會出現死循環。
    最後,若是a[left,right)區域範圍內沒找到, 就返回-1
相關文章
相關標籤/搜索