算法思维(二):分治

分治:如何利用分治法完成数据查找

核心思想就是“分而治之”
把一个大规模、高难度的问题,分解为若干个小规模、低难度的小问题
小问题的答案合并,来得到原问题的答案

很多高效率的算法都是以分治法作为其基础思想,例如快速排序和归并排序。

分治法是什么?

计算机求解问题所需最小时间,与其涉及的数据规模强相关

比如:在一个包含 n 个元素的无序数组中,要求按照从小到大的顺序打印其 n 个元素。

n 个元素之间的两两比较的计算方法,得到从小到大的序列
当数据量 n = 1 时,不需任何计算,直接打印即可;
当数据量 n = 2 时 ,那需要做 1 次比较即可达成目标;
当数据量 n = 3 时,要对这 3 个元素进行两两比较,共计 3 次比较;
而当数据量 n = 10 时,问题就不那么容易处理了,我们需要 45 次比较(计算方式是 0.5*n(n-1) )。

利用分治法,先将一个难以直接解决的大问题,分割成一些可以直接解决的小问题。
仍然无法直接解决,继续递归地分割,直到每个小问题都可解

子问题 互相独立、形式相同。可以采用同一种解法,递归地去解决这些子问题,再将每个子问题的解合并,就得到了原问题的解

分治法的价值

误解:计算机性能还不错,采用分治法相对于全局遍历没有什么差别

例如:在 1000 个有序数字构成的数组 a 中,判断某个数字 c 是否出现过

第一种方法,全局遍历。 复杂度 O(n)。
采用 for 循环,对 1000 个数字全部判断一遍。

第二种方法,采用二分查找。 复杂度 O(logn)。
递归地判断 c 与 a 的中位数的大小关系,并不断缩小范围。

对时间的消耗几乎一样。那分治法的价值又是什么呢
大数据集上,分治法的价值才能显现出来

例子:假如有一张厚度为 1 毫米且足够柔软的纸,问将它对折多少次之后,厚度能达到地球到月球的距离?
只需要对折 39 次

在数组 a 中查找数字 c ,数组 a 的大小拓展到 549,755,813,888 这个量级上,使用第二种的二分查找方法,仅仅需要 39 次判断

复杂度为 O(logn) 相比复杂度为 O(n) 的算法,在大数据集合中性能有着爆发式的提高


分治法的使用

分治法特征:

1、难度在降低
原问题的解决难度,随着数据的规模的缩小而降低。

2、问题可分
题可以分解为若干个规模较小的同类型问题

3、解可合并(分治法完全取决于这个特征)
利用所有子问题的解,可合并出原问题的解

4、相互独立
一个子问题的求解不会影响到另一个子问题
否则分治法需要重复地解决公共子问题,效率低下

分治法需要递归地分解问题,再去解决问题
分治法在每轮递归上,都包含:分解问题、解决问题、合并结果


二分查找

二分查找利用分治法去解决查找问题,需要一个前提,那就是输入的数列是有序的。

  1. 选择标志 i 将集合 L 分为二个子集合,一般使用中位数;
  2. 判断标志 L(i) 是否能与要查找的值 des 相等,相等直接返回结果;
  3. 如果不相等,需要判断 L(i) 与 des 的大小;
  4. 基于判断的结果决定下步是向左查找还是向右查找。如果向某个方向查找的空间为 0,则返回结果未查到;
  5. 回到步骤 1。

我们对二分查找的复杂度进行分析。二分查找的最差情况是,不断查找到最后 1 个数字才完成判断。那么此时需要的最大的复杂度就是 O(logn)。

分治法案例

在数组 { 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,确定查找范围
1、最初 low 为 0,high 为数组长度减 1。

2、在一个循环体内,判断 low 到 high 的中位数与目标变量 targetNumb 的大小关系。

3、根据结果确定向左走(high = middle - 1)或者向右走(low = middle + 1),来调整 low 和 high 的值。

4、直到 low 反而比 high 更大时,说明查找不到并跳出循环。我们给出代码如下:

package divide_conquer;

/**
 * @author hym
 * @date 2021/11/8
 * @description
 */
public class BinarySearch {
    public static void main(String[] args) {
        // 需要查找数字
        int targetNumber = 8;
        // 目标有序数组
        int[] sortedArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int middleIndex = 0;
        int lowIndex = 0;
        int highIndex = sortedArray.length - 1;
        boolean isFind = false;

        while (lowIndex < highIndex) {
            middleIndex = (highIndex + lowIndex) / 2;
            if (sortedArray[middleIndex] == targetNumber) {
                System.out.println(targetNumber + "下标:" + middleIndex);
                isFind = true;
                break;
            } else if (sortedArray[middleIndex] > targetNumber) {
                highIndex = middleIndex - 1;
            } else {
                lowIndex = middleIndex + 1;
            }
        }
        if (!isFind) {
            System.out.println(targetNumber + "不存在");
        }
    }
}

  1. 二分查找的时间复杂度是 O(logn),这也是分治法普遍具备的特性。
    当你面对某个代码题,而且约束了时间复杂度是 O(logn) 或者是 O(nlogn) 时,可以想一下分治法是否可行

  2. 二分查找的循环次数并不确定。一般是达到某个条件就跳出循环。
    因此,编码的时候,多数会采用 while 循环加 break 跳出的代码结构

  3. 二分查找处理的原问题必须是有序的。
    因此,当你在一个有序数据环境中处理问题时,可以考虑分治法。
    相反,如果原问题中的数据并不是有序的,则使用分治法的可能性就会很低了

习题

在一个有序数组中,查找出第一个大于 9 的数字,假设一定存在。例如,arr = { -1, 3, 3, 7, 10, 14, 14 }; 则返回 10。

在这里提醒一下,带查找的目标数字具备这样的性质:

第一,它比 9 大;
第二,它前面的数字(除非它是第一个数字),比 9 小。

因此,当我们作出向左走或向右走的决策时,必须满足这两个条件。

package divide_conquer;

/**
 * @author hym
 * @date 2021/11/8
 * @description 在一个有序数组中,查找出第一个大于 9 的数字,假设一定存在。
 * 例如,arr = { -1, 3, 3, 7, 10, 14, 14 }; 则返回 10。
 */
public class Practice {
    public static void main(String[] args) {
        int targetNum = 9;
        int[] sortedArr = {-1, 3, 3, 7, 10, 14, 14};
        int lowIdx = 0;
        int highIdx = sortedArr.length - 1;
        int middleIdx = 0;

        while (lowIdx < highIdx) {
            middleIdx = (lowIdx + highIdx) / 2;
            if (sortedArr[middleIdx] > 9 && (middleIdx == 0 || sortedArr[middleIdx - 1] <= 9)) {
                // 数字在middle处
                System.out.println(targetNum + " " + middleIdx);
                break;
            } else if (sortedArr[middleIdx] > targetNum) {
                // 数字在low middle之间
                highIdx = middleIdx - 1;
            } else {
                // 数字在 middle high之间
                lowIdx = middleIdx + 1;
            }
        }
    }
}

总结

面对陌生问题,注意原问题的数据是否有序,预期时间复杂度是否带有logn,是否可以通过小问题合并出原问题答案。均满足,应该考虑分治法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值