算法七——分治算法

文章出处:极客时间《数据结构和算法之美》-作者:王争。该系列文章是本人的学习笔记。

MapReduce本质就是一个分值算法。

什么是分治算法

分治算法的核心是:分而治之。也就是将原问题分解为n个规模较小,并且结构与原问题相似的子问题,递归地解决这些子问题,并且合并子问题的结果得到原问题的解。

与递归的区别:递归是一种编程技巧,分治是一种算法思想。

使用分治法的步骤:
1 分解:将原问题分解为一系列子问题;
2 解决:递归地解决各个子问题,当问题足够小,可以直接求解;
3 合并:将子问题的结果合并为原问题的解。

分治算法能解决的问题的特征:
1 原问题与分解后的子问题具有相同的模式。
2 子问题可以独立求解,子问题之间没有相关性;这点与动态规划是有区别的。动态规划分解后的子问题可能是重复的,需要把结果保存下来,避免重复计算。
3 具有分解终止条件。当子问题足够小,可以直接求解。
4 子问题的解合并为原问题的解,合并操作不能太复杂,否则起不到降低复杂度的效果。

计算数组的逆序度

假设我们有n个数据,希望从小到大排序。当数组完全有序的时候,有序度为 n ( n 1 ) 2 \dfrac{n(n-1)}{2} ,逆序度为0。当数组是按照从大到小排序的时候,那有序度是0,无序度是 n ( n 1 ) 2 \dfrac{n(n-1)}{2} 。除了这两种极端情况,我们计算数组逆序对的个数表示逆序度。

如何编程求出一组数组中逆序对的个数呢?

直观的想法就是从第0个元素开始,算一个后面有几个元素比它小,计数为k0。再从第1个元素开始,算一个后面有几个元素比它小,计数为k1…一直算到最后一个元素。这几个计数(k0,k1…)加和,就是逆序对的个数。时间复杂度是O(n^2)。是不是可以改进呢?

我们试着用分治思想解决。求数组A的逆序对个数,可以分解为前后两个部分数组分别标记为A1,A2。递归求解A1,A2逆序对个数K1,K2,再求出A1与A2之间逆序对个数K3。K1+K2+K3就是原问题的解。

当数组中只有两个元素的时候,就可以知道K1,K2的值。
如何求K3。我们可以参考归并排序的合并操作。将数组A1,A2排序。假设数组A1={1,5,6},A2={2,3,4}。A1长度为3。
合并排序:
1(来自A1数组,不用管)
1,2 (来自A2数组,此时A1数组还有3-1个元素没有排序,所以2小于A1数组中的2个元素)
1,2,3(来自A2数组,此时A1数组还有3-1个元素没有排序,所以2小于A1数组中的2个元素)
1,2,3,4(来自A2数组,此时A1数组还有3-1个元素没有排序,所以2小于A1数组中的2个元素)
1,2,3,4,5(来自A1数组,不用管)
1,2,3,4,5,6(来自A1数组,不用管)
最终:此次合并操作发现逆序对个数是2+2+2=6。
合并为原问题的解:K1+K2+6。
合并操作是一个O(n)的时间复杂度。

public class ArrayReverseCount {
    private int num = 0;
    public int count(int[] a){
        int n = a.length;
        return mergeSortCounting(a,0,n-1);
    }

    private int mergeSortCounting(int[] a, int start, int end) {
        if(start>=end){
            return 0;
        }
        int q = (start+end)/2;
        int k1 = mergeSortCounting(a,start,q);
        int k2 = mergeSortCounting(a,q+1,end);
        int k3 = merge(a,start,q,end);
        return k1+k2+k3;
    }

    private int merge(int[] a, int start, int q, int end) {
        int[] temp = new int[end-start+1];
        int i= start;
        int j= q+1;
        int k = 0;
        int nums = 0;
        while(i<=q && j<=end){
            if(a[j]<a[i]){
                temp[k++]=a[j++];
                nums += q-i+1;
            }else{
                temp[k++] = a[i++];
            }
        }
        while(i<=q){
            temp[k++] = a[i++];
        }
        while(j<=end){
            temp[k++] = a[j++];
        }
        for(i=0;i<=end-start;i++){
            a[start+i]= temp[i];
        }
        return nums;
    }
}

经典练习题目

二维平面上有 n 个点,如何快速计算出两个距离最近的点对?
有两个 nn 的矩阵 A,B,如何快速求解两个矩阵的乘积 C=AB?
自己练习

分治思想处理海量数据

前面学到的一些算法和数据结构,都是基于内存存储和单机处理的。如果要处理的数据量大,没有办法一次加载到内存中。这时候这些算法和数据结构就不能发挥作用了。但我们可以使用分治思想来处理这个问题。

例如我们需要对10G的订单按照金额排序。单机只有3G内存。那么,我们可以把这10G订单扫描一遍,找到订单金额的最小值和最大值。将这10G订单按照订单金额从小到大分成几个区间。例如1-100放入一个小文件,101-200放入另外一个文件。以此类推。这样形成的一个一个小文件可以加载到内存中。对单个文件最排序,排序之后再合并排序结果。

如果订单数据是放在GFS这样的分布式文件系统上。被分成的多个小文件可以同时被不同的机器加载处理,最后再合并结果集。这样并行处理速度就快多了。这里需要注意一点:数据存储的机器与处理的机器需要在同一个网段内或者局域网。否则数据传输速度会是最大的时间开销。反而会慢。

Mapreduce 与 分治算法

这也就是MapReduce的原理。单个机器的性能不足以完成任务,就把任务分配到多台服务器 。最后再合并结果。MapReduce是一个任务调度 框架。数据依赖GFS存储,依赖Borg管理机器。它从 GFS 中拿数据,交给 Borg 中的机器执行,并且时刻监控机器执行的进度,一旦出现机器宕机、进度卡壳等,就重新从 Borg 中调度一台机器执行。

发布了148 篇原创文章 · 获赞 35 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/flying_all/article/details/98631772
今日推荐