最近正在看《Spark大数据处理:技术、应用与性能优化》这本书,然后对于最后一章的编程实战比较感兴趣。但是上面写的算法个人觉得还不是很简洁,无法体现出scala的优点,所以稍作了一些修改,仅供参考。
设计思路
海量数据求中位数有很多解决方案。 假设海量数据已经预先排序本例的解决方案为:将
整个数据空间划分为K个桶。 第一轮,在mapPartition阶段先将每个分区内的数据划分为K个
桶,统计桶中的数据量,然后通过reduceByKey聚集整个RDD每个桶中的数据量。 第二轮,
根据桶统计的结果和总的数据量,可以判读数据落在哪个桶里,以及中位数的偏移量
(offset)。 针对这个桶的数据进行排序或者采用Top K的方式,获取到偏移为offset的数
据。
代码实现:
import org.apache.spark.{SparkConf, SparkContext}
object Wedian {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("Wedian")
.setMaster("local")
val sc = new SparkContext(conf)
//读取数据
val dataRDD = sc.textFile("./data/num.txt")
.flatMap(_.split(" "))
.map(x => x.toInt)
//将一定范围内的数据分区,比如数据1,2,3,4,5,6 如果num/4会将1,2,3放入一个分区,4放入一个分区,5,6放入一个分区。相当于将归并,减少计算量,切记不能用求余,目的为了保证数据的有序性。
val mappedDataRDD = dataRDD.map(num => (num / 4, num))
.sortByKey()
//统计每个分区元素个数
val countRDD = dataRDD.map(num => (num / 4, 1))
.reduceByKey((a, b) => (a + b)).sortByKey()
//将RDD转换成map形式,用于后续计算
val data_count = countRDD.collectAsMap()
//计算元素总个数
val sum = data_count.map(s => s._2).sum
//中位数所在分区的末尾元素的偏移量
var temp = 0
//中位数前一个分区的末尾元素的偏移量
var temp_1 = 0
//所处在第几个分区
var index = 0
//中位数的偏移量
var mid = 0
//计算中位数在整个元素的位置
if (sum % 2 != 0) mid = sum / 2 + 1 else mid = sum / 2
//分区总个数
val count = countRDD.count()
var flag = 1
//scala中没有break语句,所以利用守卫跳出循环
for (i <- 0 to count.toInt - 1 if flag == 1) {
temp = temp + data_count(i)
temp_1 = temp - data_count(i)
if (mid <= temp) {
index = i
flag = 0
}
}
println(mid +" "+ index +" "+temp+" "+temp_1)
//计算中位数在当前分区的偏移量
val offset = mid - temp_1
//利用过滤将这个分区内的所有数取出来,将中位数以前的该分区内的所有数取出来,再取最后一个数既是中位数
val result = mappedDataRDD.filter(x => x._1 == index).takeOrdered(offset)
println(result(offset-1)._2)
sc.stop()
}
}