分治法作为一种常见的算法思想,其概念为:把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
分治法的策略是:对于一个问题,若该问题可以容易地解决(比如说规模较小)则直接解决,否则将其分解为若干个规模较小的子问题(这些子问题互相独立且与原问题形式相同),递归地求解这些子问题,然后将各子问题的解合并得到原问题的解。
可以用分治法解决的问题一般有如下特征:
1>问题的规模缩小到一定的程度就可以容易地解决。此特征是大多数问题所具备的,当问题规模增大时,解决问题的复杂度不可避免地会增加。
2>问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。此特征也较为常见,是应用分治法的前提。
3>拆分出来的子问题的解,可以合并为该问题的解。这个特征在是否采用分治法的问题上往往具有决定性作用,比如棋盘覆盖、汉诺塔等,需要将子问题的解汇总,才是最终问题的解。
4>拆分出来的各个子问题是相互独立的,即子问题之间不包含公共的子问题。该特征涉及到分治法的效率,如果各子问题是不独立的,则需要重复地解公共的子问题,此时用动态规划法更好。
使用分治法的基本步骤:
1>分解,将原问题分解为若干个规模较小、相互独立、与原问题形式相同的子问题。
2>解决,若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题。
3>合并,将各个子问题的解合并为原问题的解。
二分查找是一种常见的高效查找方式,也是常见的分治法的例子。同时,二分查找有着较为严格的要求,即二分查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
二分查找的大致查找过程为:假设线性表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功。否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,查找成功;或直到子表不存在为止,此时查找不成功(查找关键字不在线性表中)。
二分查找用Java代码表示为:
int[ ] arr = {2,3,5,8,10};
int result = binarySearch(arr,0,arr.length-1,5);
System.out.println("result:"+result);
private static int binarySearch(int[ ] arr,int start,int end,int target){
if(end < start){
return -1;
}
if(start <= end)
{
int mid = (start + end)/2;
if(target == arr[mid])
{
return mid;
}else if(arr[mid] > target)
{
return binarySearch(arr, start, mid-1,target);
}
else
{
return binarySearch(arr, mid+1,end,target);
}
}
return -1;
}
汉诺塔也是典型的使用分治法的例子之一,该问题源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
该问题的分析过程,可以用这样一句话简单概括,将1-n个盘子,借助B塔,实现从A塔到C塔的搬运。用分析的思想来解释的话,就是如下三步:
1>分:将第1到n-1个盘子从A塔搬到B塔;
2>治:将第n个盘子从A塔搬到C塔;
3>循环地分治:将第n-1个盘子从B塔搬到C塔。
Java代码如下:
public static void main(String[] args) {
hanoi(3, "A", "B", "C");
}
public static void hanoi(int n, String source, String temp, String target) {
if (n == 1) {
//如果只有1个盘子,直接从source移动到target
move(n, source, target);
} else {
//将第1个盘子到第n-1个盘子由source经过target移动到temp,继续递归
hanoi(n - 1, source, target, temp);
//移动盘子n由source移动到target,实现移动
move(n, source, target);
//把之前移动到tempTower的第1个盘子到第n-1个盘子n-1,由temp经过source移动到target,继续递归
hanoi(n - 1, temp, source, target);
}
}
//第n个盘子的从source移动到target
private static void move(int n, String source, String target) {
System.out.println("第" + n + "号盘子,从:" + source + " 移动到 " + target);
}
归并排序是另一个常见的使用分治思想的例子,针对这个问题,网上有个经典的图解,如下:
从这个图能看出来,归并排序就是个不断拆分、最后再组合的过程,用分治的说法描述是:
1>分:不断拆解、直到每个子数组数量为1的左右子数组。
2>治:左右子数组不断合并。
分拆的过程比较好理解,就是不断通过递归来实现数组“由大到小”的变化过程。合治的过程需要分情况讨论:
1>两个子数组恰好一组一个进行合并;
2>左边子数组已经合并完了,此时只需要将右边子数组插入到有序数组中;
3>右边子数组已经合并完了,此时只需要将左边子数组插入到有序数组中。
Java代码如下:
//归并排序
public static void main(String[] args) {
int[ ] arr = {5,3,1,6,2};
int[ ] temp = new int[ 5 ];
mergeSort(arr,0,arr.length-1,temp);
for(int i=0; i < arr.length; i++)
System.out.print(arr[ i ]+" ");
}
//将两个有序数组合并排序
private static void mergeArray(int a[ ],int first,int mid,int last,int temp[ ])
{
int i=first,j=mid+1;
int m=mid,n=last;
int k=0;
//两个子数组同时遍历
while(i <= m && j <= n)
{
if(a[ i ] < a[ j ])
temp[ k++ ]=a[ i++ ];
else
temp[ k++ ]=a[ j++ ];
}
//遍历前半个子数组
while(i <= m)
temp[ k++ ]=a[ i++ ];
//遍历后半个子数组
while(j <= n)
temp[ k++ ]=a[ j++ ];
//赋值给原数组
for(i=0; i < k; i++)
a[ first+i ]=temp[ i ];
}
//将两个任意数组合并排序
private static void mergeSort(int[ ] arr,int first,int last,int[ ] temp)
{
if(first < last)
{
int mid=(first+last)/2;
mergeSort(arr,first,mid,temp); //递归处理左边子数组
mergeSort(arr,mid+1,last,temp); //递归处理右边子数组
mergeArray(arr,first,mid,last,temp); //将由原始数组分隔而成的两个有序子数组合并
}
}