【算法】分治的思路
一、算法理解
分治,字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
如果想用分治,需要下面的几个条件:
1)该问题的规模缩小到一定的程度就可以容易地解决;
2)该问题可以分解为若干个 规模较小的相同子问题;
3)利用该问题分解出的子问题的解可以 合并为该问题的解;
4)该问题所分解出的各个子问题是 相互独立 的,即子问题之间不包含公共的子问题。(非必需)
具体场景分析
- 第一条是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
- 第二条是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;
- 第三条是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备 第三条特征,则可以考虑用贪心法或动态规划法;
- 第四条不是必要条件,但涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地 解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。(这一点是分治算法跟动态规划的明显区别。 可以说,动态规划法的实质是: 分治算法思想 + 解决子问题冗余情况。)
分治法在每一层递归上都有三个步骤:
- step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
- step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
- step3 合并:将各个子问题的解合并为原问题的解。
【分治和动态规划的区别】:
- 分治策略分解的子问题是相互独立且与原问题相同的。很多时候原问题分解成两个子问题多数情况下也是行之有效的。
- 动态规划:也是把一个复杂的问题分成若干个子问题,但与分治法不同,适用动态规划的问题分解后的子问题通常是不互相独立的。这时候,若还用分治的话,会因为子问题太多以至于最后解决问题需要耗费指数级的时间。
一般动态规划步骤分为:
1)刻画最优解的结构特征;
2)递归地定义最优值;
3)以底向上的方法计算最优值;
4)构造最优解;
四、分治使用案例
1) 二分查找
不赘述。
2)归并排序
A. 自顶而下的归并排序
自顶向下的归并排序就是把数组元素不断的二分,直到子数组的元素个数为1,然后将两个有序的子序列合并成一个新的有序序列,两个新的有序序列又可以合并成另一个新的有序序列,以此类推,直到合并成一个有序的数 组。
代码实现参考:
public class Solution {
private int[] tmpArray;
private void sort(int[] data) {
// 创建好一个临时数组,避免排序过程中重复创建数组
tmpArray = new int[data.length];
mergeSort(data, 0, data.length - 1);
}
/* 方式一:自顶而下的归并排序
*/
private void mergeSort(int[] data, int start, int end) {
// 分解到最子序列是1个元素,不需要排序,直接返回
if (start == end) {
return;
}
// 需要排序序列拆分成2个序列。两个子序列分别排序后,合并。
int mid = start + (end - start) / 2;
mergeSort(data, start, mid);
mergeSort(data, mid + 1, end);
// 合并排序后的两个序列
merge(data, start, mid, end);
}
/*
* 合并两个有序序列。
* 分别用lfe指向左序列索引,right指向右序列索引,比较大小,放入排序后目标序列
*/
private void merge(int[] data, int start, int mid, int end) {
System.arraycopy(data, start, tmpArray, start, end - start + 1);
int left = start;
int right = mid + 1;
for (int i = start; i <= end; i++) {
if (left > mid) {
data[i] = tmpArray[right++];
} else if (right > end) {
data[i] = tmpArray[left++];
} else if (tmpArray[left] < tmpArray[right]) {
data[i] = tmpArray[left++];
} else {
data[i] = tmpArray[right++];
}
}
}
}
B.自底而上的归并排序
自底向上的归并排序算法的思想就是把数组从头开始,临近的两个元素归并成两两有序的序列,两两有序的序列再 归并成四个有序的序列,以此类推,直到整个数组有序。需要注意的是数组按照归并长度划分,最后一个子数 组可能不满足长度要求,这个情况需要特殊处理。自顶向下的归并排序一般用递归来实现,而自底向上的一般用循 环来实现。
代码参考:
public class MergeSort {
private int[] tmpArray;
private void sort(int[] data) {
// 先创建好一个临时数组,避免排序过程中重复创建数组
tmpArray = new int[data.length];
mergeSort(data, 0, data.length - 1);
}
private void mergeSort2(int[] data, int start, int end) {
//排序步长,从最小长度为1的两个子序列排序开始
int step = 1;
while (step <= end) {
for (int i = start; i <= end; i += step * 2) {
int innerStart = i;
int innerEnd = i + step * 2 - 1;
int innerMid = i + step - 1;
if (innerEnd > end) {
innerEnd = end;
}
if (innerMid > end) {
innerMid = end;
}
merge(data, innerStart, innerMid, innerEnd);
}
step *= 2;
}
}
/*
* 合并两个有序序列。
* 分别用lfe指向左序列索引,right指向右序列索引,比较大小,放入排序后目标序列
*/
private void merge(int[] data, int start, int mid, int end) {
System.arraycopy(data, start, tmpArray, start, end - start + 1);
int left = start;
int right = mid + 1;
for (int i = start; i <= end; i++) {
if (left > mid) {
data[i] = tmpArray[right++];
} else if (right > end) {
data[i] = tmpArray[left++];
} else if (tmpArray[left] < tmpArray[right]) {
data[i] = tmpArray[left++];
} else {
data[i] = tmpArray[right++];
}
}
}
}