分治策略:将原问题划分成n个规模较小而结构与原问题相似的子问题,然后递归地解决这些子问题,最后再合并其结果,就可以得到原问题的解。
它需要三个步骤:
- 分解:将原问题分解成一系列的子问题。
- 解决:递归地解决各个子问题。若子问题足够小,则直接求解。
- 合并:将子问题的结果合并成原问题的解。
通过分治策略和分治步骤,可以简单地默出归并算法。
- 分解:将n个元素分成各自包含n/2个元素的子序列
- 解决:用归并排序法递归地对两个子序列进行排序。
- 合并:合并两个以排序的子序列,得到排序结果:
书上的变量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个最小元素,并且是排好序的。
- 初始化:在for循环开始之前,k = left。因此子数组a[left..k)是空的。这个空数组包含了L和R中k-left个最小元素,也就是0个元素。此外,i,j都是0,因此L[i], R[j]都是各自所在数组中,尚未被复制会数组A的最小元素。
- 保持:分两种情况:
- 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个最小元素,并且是排好序的.
- L数组为空,或者L、R数组都不为空且L[i] > R[j]: R[j]就是会被复制回数组a的最小元素。将R[j]复制到a[k]后,子数组a[left..k)将包含k-left+1个最小元素,并且是已经排好序的.
- 终止: 当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中。
边界条件:
- 因C语言无法表达书中伪代码中的无穷大哨兵。因此必须显式地判断L、R数组不为空。
- 书中伪代码数组下标是从1开始的,而C语言的下标是从0开始的。因此当left = 0, right=1时,计算m时会出现m=0+(1-0)/2 = 0的情况.从而陷入死循环。因此mergeSort的退出条件不能是left >= right, 而必须是right - left < 2。即left与right中间只有一个元素是即可退出.
- 取中通常算法是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)中.
- 初始化: 给出的是全量数组,因此成立.
- 保持:
- 查找到target,直接返回下标.
- 如果a[m] <target, 则a[left,m+1)中必然没有target存在. 因此可将查找范围缩小至a[m+1,right)
- 同理, 可将查找范围缩小至a[left,m).
由此,我们每次都确保了target必定在我们的查找范围内,落在a[left,right)区域范围内
- 终止: 如果没有找到的待查区域是必定会至少减少1个长度。也就是说程序必定会正确的终止不会出现死循环。
最后,如果a[left,right)区域范围内没找到, 就返回-1