算法基础:分治
基本概念
分治法的核心思想就是“分而治之”。利用分而治之的思想,就可以把一个大规模、高难度的问题,分解为若干个小规模、低难度的小问题。然后,在把这些简单问题解决好之后,通过把这些小问题的答案合并,就得到了原问题的答案。通常而言,这些小问题具备互相独立、形式相同的特点。
很多高效率的算法都是以分治法作为其基础思想,例如排序算法中的快速排序和归并排序。
算法思想
当需要采用分治法时,一般原问题都需要具备以下几个特征。
- 难度在降低:即原问题的解决难度,随着数据的规模的缩小而降低。这个特征绝大多数问题都是满足的。
- 问题可分:原问题可以分解为若干个规模较小的同类型问题。这是应用分治法的前提。
- 解可合并:利用所有子问题的解,可合并出原问题的解。这个特征很关键,能否利用分治法完全取决于这个特征。
- 相互独立:各个子问题之间相互独立,某个子问题的求解不会影响到另一个子问题。如果子问题之间不独立,则分治法需要重复地解决公共的子问题,造成效率低下的结果。
分治与递归的对比:分治可以采用递归或递推来分解问题。如果分治法使用递归,那么分治法在每轮递归上,都包含了分解问题、解决问题和合并结果这 3 个步骤。
案例
二分查找
通常二分查找需要一个前提,那就是输入的数列是有序的。
二分查找的思路比较简单,步骤如下:
- 选择一个标志
i
将集合L
分为二个子集合,一般可以使用中位数; - 判断标志
L(i)
是否能与要查找的值des
相等,相等则直接返回结果; - 如果不相等,需要判断
L(i)
与des
的大小; - 基于判断的结果决定下步是向左查找还是向右查找。如果向某个方向查找的空间为 0,则返回结果未查到;
- 回到步骤 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
,确定查找范围。最初 low
为 0,high
为数组长度减 1。在一个循环体内,判断 low
到 high
的中位数与目标变量 targetNumb
的大小关系。根据结果确定向左走(high = middle - 1
)或者向右走(low = middle + 1
),来调整 low
和 high
的值。直到 low
反而比 high
更大时,说明查找不到并跳出循环。
注意:当数组元素过多时,(high + low) / 2
容易造成溢出,可以用 high + (low - high) / 2;
或者无符号右移也可以避免溢出。
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;
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);
}
}
在一个有序数组中,查找出第一个大于9的数字,假设一定存在。例如,arr = { -1, 3, 3, 7, 10, 14, 14 };
则返回 10。
public static void main(String[] args) {
int targetNumb = 9;
// 目标有序数组
int[] arr = { -1, 3, 3, 7, 10, 14, 14 };
int middle = 0;
int low = 0;
int high = arr.length - 1;
while (low <= high) {
middle = (high + low) / 2;
if (arr[middle] > targetNumb && (middle == 0 || arr[middle - 1] <= targetNumb)) {
System.out.println("第一个比 " + targetNumb + " 大的数字是 " + arr[middle]);
break;
} else if (arr[middle] > targetNumb) {
// 说明该数在low~middle之间
high = middle - 1;
} else {
// 说明该数在middle~high之间
low = middle + 1;
}
}
}
总结
二分查找的一些经验和规律的总结:
- 二分查找的时间复杂度是
O(logn)
,这也是分治法普遍具备的特性。当你面对某个代码题,而且约束了时间复杂度是O(logn)
或者是O(nlogn)
时,可以想一下分治法是否可行。 - 二分查找的循环次数并不确定。一般是达到某个条件就跳出循环。因此,编码的时候,多数会采用
while
循环加break
跳出的代码结构。 - 二分查找处理的原问题必须是有序的。因此,当你在一个有序数据环境中处理问题时,可以考虑分治法。相反,如果原问题中的数据并不是有序的,则使用分治法的可能性就会很低了。
分治法经常会用在海量数据处理中。这也是它显著区别于遍历查找方法的优势。在面对陌生问题时,需要注意原问题的数据是否有序,预期的时间复杂度是否带有 logn
项,是否可以通过小问题的答案合并出原问题的答案。如果这些先决条件都满足,就应该第一时间想到分治法。
- SpringBoot就是这么简单
- SpringData JPA就是这么简单
- Openflow细节理解之—Buffer_id篇
- 移动商城项目总结
- 移动商城第一篇【搭建项目环境+数据模型】
- 移动商城第二篇(品牌管理模块)【文件上传、数据校验、CRUD】
- 手把手在亚马逊EC2上搭建Keras GPU
- 移动商城第三篇(商品管理)【查询商品、添加商品】
- 移动商城第四篇(商品管理)【添加商品续篇、商品审核和上下架、前台展示、后台筛选】
- 移动商城第五篇(用户模块)【用户登陆、回显用户、拦截器、收货地址】
- 移动商城第六篇【单品查询、静态化页面】
- 移动商城第七篇【购物车增删改查、提交订单】
- Shiro入门这篇就够了【Shiro的基础知识、回顾URL拦截】
- OFTest(一):如何忽略一些字段在端口poll报文
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- JUnit 5基础指南
- 记录一则expdp任务异常处理案例
- Jenkins打造多分支流水线指南
- Spring中的设计模式
- java单元/集成测试中使用Testcontainers
- AOP编程简介及其在Spring框架中的使用
- 使用Maven Archetype创建Java项目模板
- left join左表一定是驱动表吗?
- OneFlow快速上手教程
- [译]HAL-超文本应用语言
- OneFlow | 新深度学习框架后浪(附源代码)
- 居民身份证阅读器产品开发学习心得(再谈标准-软件-协议)
- Spring Data REST不完全指南(一)
- Cocos2d-js中的简易MVC框架(五)MVC框架的使用
- [译]谈谈SpringBoot 事件机制