分布式集群中大数据的中位数

问题
面试时经常被问到的一个问题:几万亿的数据分布到几千台网络连接的计算机中,怎么最少的数据交换,最快的速度找到这些数据的中位数?(备注:看看候选人是否愿意澄清题意,数据是什么类型?计算机是怎么连接的?顺便考考网络。)

首先,看看什么是中位数。通用的定义是,给出一个排序好的列表,中间的那个元素就是中位数。比如,对有11个元素的排序好的列表[1 1 2 3 3 4 5 5 5 6 9],中位数就是离左右各5个元素位置的中间的那个元素,它的值就是列表中的4。(备注:如果候 选人不知道中位数,通过简单的例子,看看候选人是否能够快速理解和学习。这儿,可以考考候选人,如果是偶数个元素时,他会怎么做。)

那么这道题为什么值得作为一道面试题呢?

让我们来进行一些简单的估算,一万亿(10^12)个整数,按每个整数4个字节,4x10^12字节=4x10^9KB=4x10^6MB=4x10^3GB,如果每个机器内存4GB的话,需要存放到1000台机器上。所以,不是简单的排序就好解决的问题。。。(备注:这些看是简单的估算,就可以用来考察候选人的计算机基础知识。)

算法

分区和支点


只是为了求中位数而做一个完全排序,毫不奇怪的有点浪费。我们所需要的只是找到中间点的一个元素,离左右都有同等数量的元素。

解决这种问题,常用的操作是分区(partition),是著名的排序算法快排(quicksort)的基础。(备注:可以问问候选人有关排序的基础问题和算法复杂度什么的。)

快排算法的过程是这样的(备注:让候选人回忆一下。),选择一个元素,称作支点(pivot),然后通过移动(shuffling/洗牌)周围的元素,将支点元素移动到到正确的排序后的位置。所有比支点小的元素移动到列表的前面,其它的,等于或是大于的元素,保持在原位,也就是列表的后面。分区之后,支点元素就在正确的排序位置了,尽管整个列表还是无序的。(备注:可以让候选人写一个partition函数看看代码功底。)接下来,可以递归的使用同样的分区方法,对支点前的元素列表,和支点后的元素列表,最后完成排序。(备注:写一个quicksort函数。)

对于找中位数,我们可以使用类似的思路:采用第一个支点分区的思路,但不用做完整排序所要求的额外工作。

顺序统计量

对于一个排序好的列表,第n个顺序统计量就是列表中的第n个位置的元素(备注:考考概率知识。)。比如,对于一个9个元素的列表,第1顺序统计量就是最小元素;对于一个m个元素的列表,第m顺序统计量就是最大元素;对于一个2m个元素的列表,第m顺序统计量就是中位数。

支点选择

完美支点


让我们来看一个例子,假如有一个列表

[4 3 1 1 5 9 2 6 5 3 5]

对于一个2m个元素的列表,我们用下划线来表示第m顺序统计量。

我们的目标是找中位数,也就是第6顺序统计量。如果我们用列表中的第一个元素,即4,来分区,列表如下

[3 1 1 2 3 4 5 9 6 5 5]

我们用红色来指示那些小于支点的元素,绿色来指示等于或大于支点的。

因为4移动到了第6个位置,我们说4是第6顺序统计量。很幸运,支点元素正好是我们寻找的中位数。所以这个支点元素4,就是中位数。

这比快排(quicksort)少了很多的工作量,因为我们不用排序子列表[3 1 1 2 3]和[5 9 6 5 5]。但是,这只是由于4碰巧是一个好的选择。但是如果分区后,支点元素不是第6顺序工作量,该怎么办?

支点在中位数位置右边

考虑支点在中位数位置右边的情形

L1 = [6 3 1 1 5 9 2 4 5 3 7]

如果用第一个元素6来分区,我们得到

L1 = [3 1 1 5 5 2 4 3 6 9 7]

这次,6移到了第9顺序统计量。因为我们找寻的是第6顺序统计量,我们知道6不是中位数。更因为第9顺序统计量在第6顺序统计量的右边,所以我们知道中位数必须小于6。因此,我们不但知道6不是中位数,中位数也不可能是9或者7。这很重要,因为我们可以完全扔掉这些值。

L2 = L1 - [6 9 7]

L2 = [3 1 1 5 5 2 4 3]

L1的第6顺序统计量,也就是中位数,这时变成了L2的第6顺序统计量。因为我们扔掉了中间值之后的所有值,我们可以继续寻找同样的顺序统计量。注意了,L2的第6顺序统计量并不是L2的中位数。L2的中位数应该是第4顺序统计量。

支点在中位数位置的左边

考虑下面的情况,支点落到了中位数位置的左边。(备注:看看候选人是否能按照前面的分析来分析。)

L1 = [2 3 1 1 5 9 3 6 5 3 5]

同样,我们寻找中位数,第6顺序统计量。 围绕着第一个元素2来分区,我们得到

L1 = [1 1 2 3 5 9 3 6 5 3 5]

这时,支点元素是第3顺序统计量,但是我们需要的是第6顺序统计量。因为第3顺序统计量在第6顺序统计量的左边,同样,1也不是中位数,可以安全的扔掉所有小于支点的元素。

L2 = L1 - [1 1]

L2 = [2 3 5 9 3 6 5 3 5]

L1的第6顺序统计量,也就是中位数,到了L2,变成了第4顺序统计量。因为我们扔掉了中间值之前的元素,我们需要调整找寻的顺序统计量。

小结

如果我们找寻第k顺序统计量,且我们选择了一个支点正好就是,那么我们找到了中位数。否则我们扔掉一些值,调整我们寻找的顺序统计量,然后继续同样操作。

边角情形一

(备注:面试时,对于高级的,我们可以考察他们是否能发现这种情形。这同样也适用于测试,或是发现bugs。)

假如列表是

L1 = [2 3 1 1 5 9 3 6 5 3 5]

我们选择了第一个元素2,作为支点且扔掉了一些元素(所有的1),得到

L2 = [2 3 5 9 3 6 5 3 5]

现在继续,选择第一个元素2,作为支点,我们没有扔掉任何元素,又得到

L3 = [2 3 5 9 3 6 5 3 5]

我们被卡住了,不能有任何进展。所以,如果支点元素又是第一个元素,我们需要转动列表,也就是将第一个元素移到最后,然后继续。

边角情形二

(备注:候选人能发现这种情况吗?)

如果所有的元素都是一样的,转动列表也不能解决问题。比如,

L1 = [3 3 3 3 3]

对于这种情况,需要特殊处理。这也简单,看看列表的最大元素是不是和最小元素一样大。如果一样,第一个元素就是中位数。

几个快捷情形

如果是找寻列表的第1顺序统计量,只要扫描最小元素。如果是找寻k个元素列表的第k顺序统计量,只要扫描最大元素。

分布式

刚才设计的算法可以做些小的修改就能工作在分布式环境中。(备注:候选人怎么推广到分布式环境中?)

实际上,将列表中的元素移动到支点之前是不必要的,我们所感兴趣的是支点在分区后在列表中所在的位置。这个可以通过计数多少元素小于支点元素,然后加1,就得到最后的位置。

我们重新回顾一下之前的例子。

支点在中位数位置的右边

L1 = [6 3 1 1 5 9 2 4 5 3 7]

如果计数有多少个元素小于6,我们得到8。所以6是第9顺序统计量,又因为第9大于第6(中位数位置),我们需要扔掉所有值大于等于6(第一个元素)的元素。

L2 = [3 1 1 5 2 4 5 3]

然后继续寻找第6顺序统计量。

支点在中位数位置的左边

L1 = [2 3 1 1 5 9 3 6 5 3 5]

如果我们计数小于2的元素的个数,得到2。所以2是第3顺序统计量,又因第3小于第6,我们需要扔掉所有小于2的元素。

L2 = [2 3 5 9 3 6 5 3 5]

在这个情况下,支点元素在完成分区后仍然是第一个位置,我们需要轮转列表。

L2 = [3 5 9 3 6 5 3 5 2]

然后继续寻找第4(6 - 2)顺序统计量。

计数但没有移动

我们看到,算法不再要求移动任何元素。那么,我们到底对列表的元素做了什么?

计数总的元素个数;
确定最小和最大元素;
计数小于某个值的元素的个数;
扔掉小于某个值的所有元素;
扔掉大于或是等于某个值的所有元素;
轮换列表的头到尾
所有这些操作都能很容易的应用到分布式环境中,此时,我们不只是处理一个列表,二是一系列列表,而且是分布着很多的机器上。唯一不容易的是轮换列表,下面我们能看到也不是那么难。

多个列表/多处理器/多机网络下实现

列表的列表

在多处理器环境实现时,我们将一个列表分拆成多个子列表(类似于一台机器上一个列表),每个子列表可以由一个处理器处理,潜在可能跨越多台机器。注意,列表的大小可能不一样。

单个列表:[1 2 3 4 5 6 7 8 9]

多个列表:[[1 2 3 4],[5 6],[7 8 9]]

回忆一下在分布式环境下的那些操作,比如计数总的元素个数;确定最小和最大元素;等等。每个这种操作会先单独作用于每个子列表,然后将结果聚合。比如,确定总的元素个数。

单个列表:length([1 2 3 4 5 6 7 8 9])=9

多个列表:sum(length([1 2 3 4]),length([5 6]),length([7 8 9]))=sum([4 2 3])=9

再比如,

单个列表:min([1 2 3 4 5 6 7 8 9])=1

多个列表:min(min([1 2 3 4]),min([5 6]),min([7 8 9]))=min([1 5 7])=1

其它必要的改变

轮换列表

单个列表时,取第一个元素作为支点。多个列表时,取第一个子列表的第一个元素。

在多个列表时,我们还需要一些修改,为了保证所有可能的支点元素都探索了。所以,在有多个子列表时,要做两个层次的轮换:首先轮换第一个子列表,然后轮换列表的列表。

轮换前:[[1 2 3],[4 5 6],[7 8 9]]

轮换后:[[2 3 1],[4 5 6],[7 8 9]]-》[[4 5 6],[7 8 9][2 3 1]]

空子列表清理

算法需要扔掉值大于或是小于某个值的元素,在单个列表时,列表不会为空,但多个列表时就有可能了。

考虑单个列表情形

[3 1 2 2 2 4 5]

支点为3,小于3的个数为4,所以3是第5顺序统计量,需要扔掉大于3的元素

结果 [3 1 2 2 2]

现在考虑多个子列表的情形

[[3 1 2],[2 2],[4 5]]

支点为3,小于3的个数为4,所以3是第5顺序统计量,需要扔掉大于3的元素

结果 [[3 1 2],[2 2],[]]

这时,我嗯需要排除空列表接着处理。

到此,针对单个列表,多个列表,分布式环境,等各种情况,思路和算法都已经完善。从解决的过程,和中间我们给出的备注,我们明白了这个为什么是一个好的面试题。


本文转载自:http://www.vccoo.com/v/841287

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值