算法导论第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个最小元素,并且是排好序的。

  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中。

 边界条件:

  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)








同样的,二分查找也是分治法的应用。应用分治步骤,可以很容易地默出二分查找法:
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

 

转载于:https://www.cnblogs.com/jackson-zhou/p/8322367.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值