我们为什么需要Map-Reduce?

在讨论我们是否真的需要Map-Reduce这一分布式计算技术之前,我们先面对一个问题,这可以为我们讨论这个问题提供一个直观的背景。

问题

这里写图片描述

我们先从最直接和直观的方式出发,来尝试解决这个问题:
先伪一下这个问题:

SELECT COUNT(DISTINCT surname) 
FROM big_name_file

我们用一个指针来关联这个文件.

接着考察每一行的数据,解析出里面的姓氏,这里我们可能需要一个姓氏字典或者对照表,然后我们可以利用最长前缀匹配来解析出姓氏。这很像命名实体识别所干的事。

拿到了姓氏,我们还需要一个链表L,这个链表的每个元素存储两个信息,一个是姓氏或者姓氏的编号,另一个是这个姓氏出现的次数。

在考察每一行的数据时,我们解析出姓氏,然后在链表L中查找这个姓氏对应的元素是否存在,如果存在就将这个元素的姓氏出现次数加一,否则就新增一个元素,然后置这个元素的姓氏出现次数为1。

当所有的行都遍历完毕,链表L的长度就是不同的姓氏的个数出现的次数。

    /**
    *  直接法伪代码
    */
    int distinctCount(file) {
    //将磁盘文件file关联到一个内存中的指针f上
    f <- file;
    //初始化一个链表
    L <- new LinkedList();
    while(true) {
        line <- f.readline();
        if(line == null)
            break;
        //解析出此行的姓氏
        surname <- parse(line);
        //如果链表中没有这个姓氏,就新增一个,如果有,就将这个姓氏的出现次数+1
        L.addOrUpdate(surname,1);
    }
    //链表的长度就是文件中不同姓氏的个数
    return L.size();
}

ok,这个方法在不关心效率和内存空间的情况下是个解决办法。
但是却有一些值得注意的问题:

在进行addOrUpdate操作时,我们需要进行一个find的操作来找到元素是否已在链表中了。对于无序链表来说,我们必须采取逐一比较的方式来实现这个find的语义。

对于上面的考虑,显然我们知道如果能按下标直接找出元素就最好不过了,我们可以在常量时间找出元素并更新姓氏出现的次数。

哈希表法

对于这一点,我们可以采取哈希表来做,采取这个结构,我们可以用常量时间来找到元素并更新。

    int distinctCountWithHashTable(file) {
    //将磁盘文件file关联到一个内存中的指针f上
    f <- file;
    //初始化一个哈希表
    T <- new HashTable();
    while(true) {
        line <- f.readline();
        if(line == null)
            break;
        //解析出此行的姓氏
        surname <- parse(line);
        //如果哈希表中没有这个姓氏,就新增一个,如果有,就将这个姓氏的出现次数+1
        T.addOrUpdate(surname,1);
    }

    //哈希表中实际存储的元素个数就是文件中不同姓氏的个数
    return T.size();
}

假设给定文件是有序的

哈希表法看起来很美,但还是有潜在的问题,如果内存不够大怎么办,哈希表在内存中放不下。这个问题同样存在于直接法中。

想想看,如果这个文件是个排好序的文件,那该多好。
所有重复的姓氏都会连着出现,这样我们只需要标记一个计数器,每次读取一行文本,如果解析出的姓氏和上一行的不同,计数器就增1.
那么代码就像下面这样:

    int distinctCountWithSortedFile(file) {
    //将磁盘文件file关联到一个内存中的指针f上
    f <- file;
    //不同姓氏的计数器,初始为0
    C <- 0;
    //上一行的姓氏
    last_surname <- empty;
    while(true) {
        line <- f.readline();
        if(line == null)
            break;
        //解析出此行的姓氏
        surname <- parse(line);
        //如果和上一行的姓氏不同,计数器加1
        if(!last_surname.equals(surname))
            C++;
        last_surname <- surname;
    }

    return C;
}

遗憾的是,我们并不能保证给定的文件是有序的。但上面方法的优点是可以破除内存空间的限制,对内存的需求很小很小。

那么能不能先排个序呢?
肯定是可以的,那么多排序算法在。但是有了内存空间的限制,能用到的排序算法大概只有位图法和外排了吧。

位图法

假设13亿/32 + 1个int(这里设32位)的内存空间还是有的,那么我们用位图法来做。
位图法很简单,基本上需要两个操作:

    /**
    * 将i编码
    */
    void encode(M,i) {
        (M[i >> 5]) |=  (1 << (i & 0x1F));
    }
    /**
    *将i解码
    */
    int decode(M,i) {
        return (M[i >> 5]) & (1 << (i & 0x1F));
    }

假设我们采取和姓氏字典一样的编号,我们做一个自然升序,那么这个方法就像下面这样:

    int distinctCountWithBitMap(file) {
    //将磁盘文件file关联到一个内存中的指针f上
    f <- file;
    //初始化一个位图结构M,长度为13亿/32 + 1
    M <- new Array();
    //不同姓氏的个数,初始为0
    C <- 0;
    while(true) {
        line <- f.readline();
        if(line == null)
            break;
        //解析出此行的姓氏编号
        surname_index <- parse(line);
        //将姓氏编号编码到位图对应的位上
        encode(M,surname_index);        
    }

    //找出位图中二进制1的个数
    C <- findCountOfOneBits(M);

    return C;
}

ok,一切看起来很完美,但如何有效地找出位图中的二进制1的个数呢?上面使用了一个findCountOfOneBits方法,找出二进制1的个数,好吧,这是另外一个问题,但我们为了完整,可以给出它的一些算法:

int findCountOfOneBits_1(int[] array) {
    int c = 0;
    for(int i = 0 ; i < array.length; i++)
        c += __popcnt(array[i]);
    return c;
}

int findCountOfOneBits_2(int[] array) {
    int c = 0;
    for(int i = 0 ; i < array.length; i++) {
        while(array[i]) {
            array[i] &= array[i] - 1;
            c++;
        }
    }

    return c;
}

int findCountOfOneBits_3(int[] array) {
    int c = 0;
    unsigned int t;
    int e = 0;
    for(int i = 0 ; i < array.length; i++) {
        e = array[i];
        t = e
            - ((e >> 1) & 033333333333)
            - ((e >> 2) & 011111111111);

        t = (t + (t >> 3)) & 030707070707
        c += (t%63);
    }

    return c;
}

上面的算法哪种效率最高呢?老三。

合并法

ok,位图法看起来破除了内存的限制,的确如此吗?如果内存小到连位图都放不下怎么办?
不解决这个问题了!开玩笑~

既然内存严重不足,那么我们只能每次处理一小部分数据,然后对这部分数据进行不同姓氏的个数的统计,用一个 { key,count} 的结构去维护这个统计,其中key就代表了我们的姓氏, count

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值