「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。
不知道大家是否了解分治算法呢?或者大家熟不熟悉归并排序,归并排序是分治算法一个典型的应用。今天就接这篇文章带大家好好探究一下分治算法,也让大家对归并排序有更深入的了解。
分治算法就是分而治治,它的本质就是将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
通俗理解:马上冬奥会就要来了,奥运会的比赛就是典型的分治算法,金牌应该给谁呢?
我们就需要将所有参赛的奥运健儿进行分治,分成一个一个的小组进行比赛,小组中胜出的获得比分,得分高的晋级,参加更高一层的比赛,直至决赛,最厉害的获得冠军。
分治法可以解决的问题大都具有以下特征:
-
该问题的规模缩小到一定的程度就可以很容易地解决;
-
该问题可以分解为若干个规模较小的相同问题(递归);
-
通过该问题分解出的子问题的解集可以合并为该问题的解,
如果具备前两条特征,而不具备第三条特征,应该考虑贪心算法或动态规划算法解决该问题;
-
该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题,
如果各子问题是不独立的则分治法要做很多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
分治法在每一层递归上都有三个步骤:
- 拆分:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
- 计算:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;
- 合并:将各个子问题的解合并为原问题的解。
我们从归并排序看一下,究竟如何运用分治算法,下边是归并排序的原理图:
大体步骤就是
- 将一乱序的数分开,到不可再分
- 对分开的子数组进行排序(当分到不可再分,是不需要额外计算的子数组只有一个数字时,你可以认为它是有序的)
- 合并两个排序好的数组,直到获得整组数的解
下面我们来看代码(源码还是很简单的,主要要掌握分治的思想,以及如何合并两个有序子数组的算法):
package com.zhj.algorithm.sort;
import com.zhj.tool.CallBack;
import com.zhj.tool.TimeTools;
import java.util.Arrays;
/**
* 归并排序
* @author zhj
*/
public class MergeSort {
public static void main(String[] args) {
// 原数组
int[] nums = {8, 4, 5, 7, 1, 3, 6, 2};
// 归并排序
mergeSort(nums, 0, nums.length-1, new int[nums.length]);
// 排序后的数组
System.out.println(Arrays.toString(nums));
// 测试80000个随机数排序效率
int[] bigArr = new int[80000];
for (int i = 0; i < 80000; i++) {
bigArr[i] = (int) (Math.random() * 80000);
}
System.out.println("归并排序:");
TimeTools.useTime(new CallBack(){
//定义execute方法
public void execute(){
mergeSort(bigArr, 0, bigArr.length-1, new int[bigArr.length]);
}
});
}
/**
* 归并排序
* @param nums 原数组
* @param left 左边索引
* @param right 右侧索引
* @param temp 临时数组
*/
public static void mergeSort(int[] nums, int left , int right, int[] temp) {
// 递归终止条件
if (left >= right) {
return;
}
// 拆分子数组
int mid = left + (right - left)/2;
// 左边子数组
mergeSort(nums, left, mid, temp);
// 右边子数组
mergeSort(nums, mid + 1, right, temp);
// 合并左边右边两个子数组算法
merge(nums, left, mid, right, temp);
}
/**
* 合并算法
* @param nums 原数组
* @param left 左边索引
* @param mid 中间索引
* @param right 右侧索引
* @param temp 临时数组
*/
public static void merge(int[] nums, int left, int mid, int right, int[] temp) {
// 左边起点
int i = left;
// 右边起点
int j = mid + 1;
// 临时数组游标
int tempCurIndex = 0;
// 将两个有序子数组排序好的值放入零时数组中
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
temp[tempCurIndex] = nums[i];
tempCurIndex++;
i++;
} else {
temp[tempCurIndex] = nums[j];
tempCurIndex++;
j++;
}
}
// 将左子数组没有遍历完的数组依次添加到临时数组中
while (i <= mid) {
temp[tempCurIndex] = nums[i];
tempCurIndex++;
i++;
}
// 将右子数组没有遍历完的数组依次添加到临时数组中
while (j <= right) {
temp[tempCurIndex] = nums[j];
tempCurIndex++;
j++;
}
// 临时数组游标归零
tempCurIndex = 0;
// 临时数组有序部分起点
int tempLeft = left;
// 将临时数组中排序好的数据放入原数组
while (tempLeft <= right) {
nums[tempLeft] = temp[tempCurIndex];
tempCurIndex++;
tempLeft++;
}
}
}