1. 分治法概念
分治,顾名思义,分而治之。
具体来说,它先将一个难以直接解决的大问题,分割成一些可以直接解决的小问题。如果分割后的问题仍然无法直接解决,那么就继续递归地分割,直到每个小问题都可解。
分治法产生的子问题与原始问题相同,只是规模减小,反复使用分治方法,可以使得子问题的规模不断减小,直到能够被直接求解为止。
通常而言,这些子问题具备互相独立、形式相同的特点。这样,我们就可以采用同一种解法,递归地去解决这些子问题。最后,再将每个子问题的解合并,就得到了原问题的解。
2. 分治法前提
当你需要采用分治法时,一般原问题都需要具备以下几个特征:
-
难度在降低,即原问题的解决难度,随着数据的规模的缩小而降低。这个特征绝大多数问题都是满足的。
-
问题可分,原问题可以分解为若干个规模较小的同类型问题。这是应用分治法的前提。
-
解可合并,利用所有子问题的解,可合并出原问题的解。这个特征很关键,能否利用分治法完全取决于这个特征。
-
相互独立,各个子问题之间相互独立,某个子问题的求解不会影响到另一个子问题。如果子问题之间不独立,则分治法需要重复地解决公共的子问题,造成效率低下的结果。
能使用分治法解决的问题一般都具有两个显著的特点:
-
是问题可以分解为若干个规模较小的相同问题,并且这个分解关系可以用递归或递推的方式逐级分解,直到问题的规模小到可以直接求解的程度。
-
是子问题的解可以用某种方式合并出原始问题的解。这很容易理解,如果不能合并出原始问题的解,那么子问题的划分和求解就没有意义了。
2. 分治法步骤
- 分解:
将问题分解为若干个规模较小,相互独立且与原问题形式相同的子问题,确保各个子问题的解具有相同的子结构。
- 解决:
如果上一步分解得到的子问题可以解决,则解决这些子问题,否则,对每个子问题使用和上一步相同的方法再次分解,然后求解分解后的子问题,这个过程可能是一个递归的过程。
- 合并:
将上一步解决的各个子问题的解通过某种规则合并起来,得到原问题的解。
3. 递归实现分治法
递归作经常和分治法一起使用,因为问题的分解肯定不是一步到位,往往需要反复使用分治手段,在多个层次上层层分解,这种分解的方法很自然地导致了递归方式的使用。
从算法实现的角度看,分治法得到的子问题和原问题是相同的,当然可以用相同的函数来解决,区别只在于问题的规模和范围不同。通过特定的函数参数安排,使得同一个函数可以解决不同规模的相同问题,这就是递归方法的基础。
4. 分治法案例
在数组 { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } 中,查找 8 是否出现过。
首先判断 8 和中位数 5 的大小关系。因为 8 更大,所以在更小的范围 6, 7, 8, 9, 10 中继续查找。此时更小的范围的中位数是 8。由于 8 等于中位数 8,所以查找到并打印查找到的 8 对应在数组中的 index 值。
从代码实现的角度来看,我们可以采用两个索引 low
和 high
,确定查找范围。最初 low
为 0,high
为数组长度减 1。在一个循环体内,判断 low
到 high
的中位数与目标变量 targetNumb
的大小关系。根据结果确定向左走 high = middle - 1
或者向右走 low = middle + 1
,来调整 low
和 high
的值。直到 low
反而比 high
更大时,说明查找不到并跳出循环。
我们给出代码如下:
public static void main(String[] args) {
// 需要查找的数字
int targetNumb = 8;
// 目标有序数组
int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int middle = 0;
int low = 0;
int high = arr.length - 1;
int isfind = 0;
while (low <= high)
{
middle = (high + low) / 2; // high + low容易造成溢出,使用 low + (high - low) / 2
if (arr[middle] == targetNumb)
{
System.out.println(targetNumb + " 在数组中,下标值为: " + middle);
isfind = 1;
break;
}
else if (arr[middle] > targetNumb)
{
// 说明该数在low~middle之间
high = middle - 1;
}
else
{
// 说明该数在middle~high之间
low = middle + 1;
}
}
if (isfind == 0)
{
System.out.println("数组不含 " + targetNumb);
}
}
5. 规律总结
- 二分查找的时间复杂度是
O(logn)
,这也是分治法普遍具备的特性。当你面对某个代码题,而且约束了时间复杂度是O(logn)
或者是O(nlogn)
时,可以想一下分治法是否可行。 - 二分查找的循环次数并不确定。一般是达到某个条件就跳出循环。因此,编码的时候,多数会采用
while
循环加break
跳出的代码结构。 - 二分查找处理的原问题必须是有序的。因此,当你在一个有序数据环境中处理问题时,可以考虑分治法。相反,如果原问题中的数据并不是有序的,则使用分治法的可能性就会很低了。
4. 实例
字符串全排列问题:
给定一个没有重复字母的字符串,输出该字符串中字符的所有排列。假如给定的字符串是“abc”,则应该输出“abc”、“acb”、“bac”、“bca”、“cab”和“cba”六种结果。