分治法是一种很强大的算法设计方法。基本思想是:将原问题分解为几个规模小但类似于原问题的子问题,递归的求解这些子问题,然后再合并这些子问题的解来建立原问题的解。
在分治策略中,递归地求解一个问题,在每层递归中应用如下三个步骤:
(1)分解(Divide):将原问题分解为一些子问题,子问题的形式与原问题一样,只是规模更小。
(2)解决(Conquer):递归地解出子问题。如果子问题规模足够小,则停止递归,直接求解。
(3)合并(Combine):将子问题的解组合成原问题的解。
分治思想体现在编码上,往往就是递归的形式。实际在编码的时候,可以遵循如下步骤:
(1)精心设计函数原型,包括入参、出参和返回值等。
(2)考虑如何分解原问题。对于某些复杂的问题,会考虑设计一个函数来解决,此时也要设计好该函数的原型,入参、出参和返回值等。
(3)调用(1)中的函数处理各个子问题,并假设子问题已经解决了。
(4)处理子问题合并的具体细节。
(5)处理基本情况。
(6)将(5)中的代码移到函数体的前面,整理代码结构。
例子1:递归版插入排序
为了排序A[1..n],我们递归地排序A[1..n-1],然后把A[n]插入已排序的数组A[1..n-1]。
第一步:设计函数原型
void my_insertion_sort(int a[], int left, int right) //将数组a[left...right]之间的元素排序,left和right都是下标,从0开始取值. { }
第二步:分解原问题
对于递归版排序而言,这一步比较简单,略过。对于某些问题而言,这一步相当重要,必须要考虑情况这一步处理以后会对原问题造成什么影响,或者产生什么结果。比如对于快速排序,这一步就会将待排序数组分为两部分,左部分的值都小于等于右半部分的值,且中间那个元素已经在最终位置了。
第三步:调用函数解决子问题
void my_insertion_sort(int a[], int left, int right) //将数组a[left, ...right]之间的元素排序,left, 和right都是下标,从0开始取值。 { my_insertion_sort(a, left, right - 1); //调用函数给数组a[left .. right-1]之间的元素排序 //数组a[left .. right-1]已经排好序了,接下来就是将a[right]插入到a[left .. right-1]中的适当位置了。 }
第四步:合并子问题
接下来要编写的代码就是将a[right]插入到a[left .. right-1]中的适当位置,如下:
void my_insertion_sort(int a[], int left, int right) //将数组a[left, ...right]之间的元素排序,left, 和right都是下标,从0开始取值。 { my_insertion_sort(a, left, right - 1); //调用函数给数组a[left .. right-1]之间的元素排序 //将a[right]插入到a[left .. right-1]中 int j = right - 1; int temp = a[right]; while (j >= left && temp < a[j]) { a[j + 1] = a[j]; --j; } a[j + 1] = temp; //至此,a[right]已经插入到a[left .. right-1]中的适当位置了 }
第五步:处理基本情况
查看函数原型void my_insertion_sort(int a[],int left,in right); 发现当left等于right的时候,待排序区间就一个元素,直接返回。
void my_insertion_sort(int a[], int left, int right) //将数组a[left, ...right]之间的元素排序,left, 和right都是下标,从0开始取值。 { my_insertion_sort(a, left, right - 1); //调用函数给数组a[left .. right-1]之间的元素排序//将a[right]插入到a[left .. right-1]中 int j = right - 1; int temp = a[right]; while (j >= left && temp < a[j]) { a[j + 1] = a[j]; --j; } a[j + 1] = temp; //至此,a[right]已经插入到a[left .. right-1]中的适当位置了 //当left == right的时候就是基本情况,此时就直接返回了。 if(left == right) return; }
第六步:优化代码结构,将第五步的代码移到前面。
void my_insertion_sort(int a[], int left, int right) //将数组a[left, ...right]之间的元素排序,left, 和right都是下标,从0开始取值。 { //当left == right的时候就是基本情况,此时就直接返回了。 if(left == right) return; my_insertion_sort(a, left, right - 1); //调用函数给数组a[left .. right-1]之间的元素排序//将a[right]插入到a[left .. right-1]中 int j = right - 1; int temp = a[right]; while (j >= left && temp < a[j]) { a[j + 1] = a[j]; --j; } a[j + 1] = temp; //至此,a[right]已经插入到a[left .. right-1]中的适当位置了 }
或者最好是
void my_insertion_sort(int a[], int left, int right) //将数组a[left, ...right]之间的元素排序,left, 和right都是下标,从0开始取值。 { if(left < right) //处理边界条件 { my_insertion_sort(a, left, right - 1); //调用函数给数组a[left .. right-1]之间的元素排序//将a[right]插入到a[left .. right-1]中 int j = right - 1; int temp = a[right]; while (j >= left && temp < a[j]) { a[j + 1] = a[j]; --j; } a[j + 1] = temp; //至此,a[right]已经插入到a[left .. right-1]中的适当位置了 } }
例子2:归并排序
归并排序就是分治思想的典型例子。
第一步:设计函数原型:
比如要将数组a[left...right]之间的元素排序,可以设计如下原型:
void my_merge_sort(int a[],int left,in right); // left和right分别是待排序区间的左右下标,取值从0开始。 { }
第二步:分解原问题
对于归并排序而言,这一步比较简单,略过。
第三步:调用函数解决子问题:
我们将待排序区间分成两部分,并在这两部分上调用我们的函数解决它。
void my_merge_sort(int a[],int left,in right); // left和right分别是待排序区间的左右下标,取值从0开始。 { int mid = ( left + right ) / 2; //将待排序数组均分为两部分,递归解决 my_merge_sort(a,left,mid); //给数组的前半部分排序 my_merge_sort(a,mid+1,right); //给数组的后半部分排序 //数组的前、后半部分都已经排好序了,接下来就是合并了。 }
第四步:合并子问题
设计一个函数merge(int a[],int l,int m,int r)来处理两个已排序数组的合并问题,这里不给出实现。
void my_merge_sort(int a[],int left,in right); // left和right分别是待排序区间的左右下标,取值从0开始。 { int mid = ( left + right ) / 2; //将待排序数组均分为两部分,递归解决 my_merge_sort(a,left,mid); //给数组的前半部分排序 my_merge_sort(a,mid+1,right); //给数组的后半部分排序
merge(a,left,mid,right); //调用写好的函数来合并数组的前后两部分 }
第五步:处理基本情况
对于之前设计的排序函数的原型:void my_merge_sort(int a[],int left,in right); 当left等于right的时候,待排序区间就一个元素,直接返回。
void my_merge_sort(int a[],int left,in right); // left和right分别是待排序区间的左右下标,取值从0开始。 { int mid = ( left + right ) / 2; //将待排序数组均分为两部分,递归解决 my_merge_sort(a,left,mid); //给数组的前半部分排序 my_merge_sort(a,mid+1,right); //给数组的后半部分排序 merge(a,left,mid,right); //调用写好的函数来合并数组的前后两部分 //当left == right的时候就是基本情况,此时就直接返回了。 if(left == right) return; }
第六步:优化代码结构,将第五步的代码移到前面。此时的代码结构如下:
void my_merge_sort(int a[],int left,in right); // left和right分别是待排序区间的左右下标,取值从0开始。 { //当left == right的时候就是基本情况,此时就直接返回了。 if(left == right) return; int mid = ( left + right ) / 2; //将待排序数组均分为两部分,递归解决 my_merge_sort(a,left,mid); //给数组的前半部分排序 my_merge_sort(a,mid+1,right); //给数组的后半部分排序 merge(a,left,mid,right); //调用写好的函数来合并数组的前后两部分 }
或者最好是
void my_merge_sort(int a[],int left,in right); // left和right分别是待排序区间的左右下标,取值从0开始。 { if(left < right) { int mid = ( left + right ) / 2; //将待排序数组均分为两部分,递归解决 my_merge_sort(a,left,mid); //给数组的前半部分排序 my_merge_sort(a,mid+1,right); //给数组的后半部分排序 merge(a,left,mid,right); //调用写好的函数来合并数组的前后两部分 } }