算法导论第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
相关文章
相关标签/搜索