算法设计思想(4)— 分治法

1. 分治法概念

分治,顾名思义,分而治之。

具体来说,它先将一个难以直接解决的大问题,分割成一些可以直接解决的小问题。如果分割后的问题仍然无法直接解决,那么就继续递归地分割,直到每个小问题都可解。

分治法产生的子问题与原始问题相同,只是规模减小,反复使用分治方法,可以使得子问题的规模不断减小,直到能够被直接求解为止。

通常而言,这些子问题具备互相独立、形式相同的特点。这样,我们就可以采用同一种解法,递归地去解决这些子问题。最后,再将每个子问题的解合并,就得到了原问题的解。

2. 分治法前提

当你需要采用分治法时,一般原问题都需要具备以下几个特征:

  • 难度在降低,即原问题的解决难度,随着数据的规模的缩小而降低。这个特征绝大多数问题都是满足的。

  • 问题可分,原问题可以分解为若干个规模较小的同类型问题。这是应用分治法的前提。

  • 解可合并,利用所有子问题的解,可合并出原问题的解。这个特征很关键,能否利用分治法完全取决于这个特征。

  • 相互独立,各个子问题之间相互独立,某个子问题的求解不会影响到另一个子问题。如果子问题之间不独立,则分治法需要重复地解决公共的子问题,造成效率低下的结果。

能使用分治法解决的问题一般都具有两个显著的特点:

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

从代码实现的角度来看,我们可以采用两个索引 lowhigh ,确定查找范围。最初 low 为 0,high 为数组长度减 1。在一个循环体内,判断 lowhigh 的中位数与目标变量 targetNumb 的大小关系。根据结果确定向左走 high = middle - 1 或者向右走 low = middle + 1 ,来调整 lowhigh 的值。直到 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”六种结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wohu007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值