何为分治
当我们求解某些问题时,由于这些问题要处理的数据相当多,或求解过程相当复杂,使得直接求解法在时间上相当长,或者根本无法直接求出。对于这类问题,我们往往先把它分解成几个子问题,找到求出这几个子问题的解法后,再找到合适的方法,把它们组合成求整个问题的解法。如果这些子问题还较大,难以解决,可以再把它们分成几个更小的子问题,以此类推,直至可以直接求出解为止。这就是分治策略的基本思想。
解法步骤
- 分解,将要解决的问题划分成若干规模较小的同类问题
- 求解,当子问题划分得足够小时,用较简单的方法解决
- 合并,按原问题的要求,将子问题的解逐层合并构成原问题的解
归并排序
以上内容摘自百度百科,阿导很庆幸认识很多一起负重前行的人,这样才有激情去探讨更多学术上的问题,以至于不让自己滞步不前。
某一次技术分享交流上,我们的专题就是归并排序,首先使用动图展示排序过程:
算法描述
- 把长度为 n 的输入序列分成两个长度为 n/2 的子序列
- 对这两个子序列分别采用归并排序
- 将两个排序好的子序列合并成一个最终的排序序列
针对拆分的思路分为递归或者循环,至于合并逻辑一致,具体代码如下:
/**
* 合并
*
* @param left
* @param right
* @return
*/
private static int[] merge(int[] left, int[] right) {
// 合并结果的数组
int[] tmp = new int[left.length + right.length];
// 存放结果数组下标
int pox = 0;
// 左数组的下标
int leftPox = 0;
// 右数组的下标
int rightPox = 0;
// 当两个数组都没有遍历结束,需进入循环体操作
while (left.length > leftPox || right.length > rightPox) {
// 若是右边数组没有遍历结束左边数组已经结束或者左右数组都没结束,但右边数组值不大于左边数组值时,取右边数组下标,反之取左边下标值
if ((right.length > rightPox && !(left.length > leftPox))
|| (left.length > leftPox && right.length > rightPox && left[leftPox] > right[rightPox])) {
tmp[pox++] = right[rightPox++];
} else {
tmp[pox++] = left[leftPox++];
}
}
// 返回结果
return tmp;
}
递归实现
首先我们讨论的是如何通过递归实现,实现方式有很多种,首先阿导给出的拆分思路是没有考虑到空间资源的浪费(有兴趣的同学可以通过下标去处理,这样一定程度上节约了空间资源,阿导这边主要展示思路),具体代码如下:
/**
* 拆分
*
* @param array
* @return int[]
* @author
* @time 2020/4/14 :00
*/
private static int[] recursiveMethodMergeSort(int[] array) {
// 获取中间值
int mid = array.length / 2;
// 最小粒度直接返回
if (mid == 0) {
return array;
}
// 对左右递归拆分最小粒度,然后合并
return merge(mergeSort(Arrays.copyOfRange(array, 0, mid)),
mergeSort(Arrays.copyOfRange(array, mid, array.length)));
}
循环写法
递归思想很好理解,合并之前先拆分,直到最小粒度,然后进行合并,相比较而言,循环写法更难理解。阿导先贴出代码,再进行讲解
private static int[] circulateMethodMergeSort(int[] array) {
// 结束条件是步长超过字符串长度,最长为 array.length/2,外循环控制合并的长度(步长)
for (int step=1;step < array.length;step*=2) {
for (int pox = 0; pox < array.length; pox = pox + 2 * step) {
// 获取中间数
int mid = pox + step;
// 合并操作
int[] merge = merge(Arrays.copyOfRange(array, pox, mid), Arrays.copyOfRange(array, mid, Math.min(mid + step, array.length)));
// 将排好序的数组进行覆盖操作
int k = pox;
for (int index = 0; index < merge.length; index++) {
array[k++] = merge[index];
}
}
}
// 返回结果
return array;
}
循环的思路和递归思路相反,递归思路是不关心具体拆分多少,反正递归到一定粒度自然会终止,循环思路的拆分其实是根据归并思想,寻找拆分的规律(以最小粒度开始合并),即每次比较的数目都是 2n ,也就是上述的步长为什么是每次都是2倍递增,内循环每次比较的是两个 step 长度, 这里只需要注意的是内循环最后一次归并,长度可能不是 2倍 step 的情况。
分治思想很简单,代码实现起来确实有点绕脑子,只要理解了核心思想,写起来也不是那么困难。
分治思想的应用
其实分治思想的应用相当广泛,小到归并排序,大到国家治理,纵观中国历史,中国一直都是中央集权制国家,这样才能保证国家的长治久安,就拿这次疫情打比方(敬那些在抗疫一线的英雄和烈士),通过全国人民的万众一心,我们这次抗疫战争相比西方国家要顺利得多(希望全球同一条心,共同战胜这次灾难),下面描述一下这里面的分治思想。
分解
:中央政府下达命令抗疫,然后每个省和自治区以及直辖市接收到命令,将命令传达至区县,最终传达至个人求解
:个人需要自律,出门需要做好防护措施,定期量体温等等合并
:然后由地方政府(卫生局)收集疫情信息,层级上报,最终得到全国范围内的疫情实时数据。
回到咱们这个程序员这个行业,分治思想能解决什么问题呢?
-
归并排序
-
汉诺塔问题
-
八皇后问题
…
以上是一些纯学术上的应用,我相信更多的人是关注在实际项目中解决了那些问题,其实分布式相关就有很多应用,比如分库分表,我们也可以通过分治思想实现一套 maxcomputer 系统,具体简单实现思路如下:
-
存储: 海量数据存储时,通过定制的散列将数据存储到不同的机器上
-
查询:当根据某些条件去查询的时候,先将命令到派发到每一个机器上,然后将结果层级合并,最终呈现到控制台
只言片语,看起来很简单,实现起来却很复杂,里面的细节之处数不胜数,阿导在此不做赘述,在文章结尾处,留一道思考题给有兴趣的同学。
- 针对 TB 甚至 PB 级的文件,如何对里面的数据进行排序?
这个是一个宏观的题目,可以抽象成一个文件里面有 60 个数据,但每次最多只能读取到里面 6 条数据,以此为限制,实现对这个文件里面数据进行排序,阿导这边已经实现了,有兴趣可交流(我的邮箱:dongzhuxu@outlook.com)。