海量数据取中位数

若有很大一组数据,数据的个数是N(每个数占4个字节),内存大小为M个字节,其中M<4*N,使得不能在现有内存情况下通过直接排序找到这N个数的中位数。解决海量数据中取中位数的方法有两种比较简单耗时的是用堆排序,还有一种是改造后基于段的计数:


1)分区间堆排序:

在现有M大小内存情况下若最多能够造出包含p个数据的堆,则先扫描一次这N个数据找到最小的p个数,耗时O(Nlog(p)),设这p个数中最大的数是a,将堆清空,在第二轮扫描出比a大的中最小的p个数,然后在把a改为记录这p个数中最大的数,依次类推,直到计算到某一轮p个数和之前够造出的数的个数大于N/2,在这p个数中找到所有数中的中位数,耗时的地方是每轮扫描构建堆都要用了O(Nlog(p)),构造的次数为N/(2*p),所以它的时间复杂度是O(N*N*log(p)/(2*p)).


2)基于段的计数


基于段的计数指的的是对一个区间范围的计数,与计数排序不同的是后者对每一个数出现次数的计数,而前者是对出现在某一区间范围内的数计数,段的大小取决于内存大小和每段计数器所占的字节数,而计数器大小又受总数据量有关,需满足计数器的最大计数能够大于最大个数N,比如本题中内存大小是M个字节,而N的大小至少需要用logN位来表示,设k个字节表示的无符号整数可以最大值大于N,则放在内存中计数器个数是M/k,设q=M/k,即这N个数分为q个段,每一段的大小是n=N/q,第一段数的范围是0~n-1,第二段是n~2*n-1,依此类推。现在我们可以从硬盘中逐个扫面这N个数,根据每个数的大小来修改他们对应范围的计数器,需要用O(N)时间完成对这q个段的计数。然后从第一段开始往后扫描累加每个段的计数器,直到累加到某一段的计数器使得它和这之前的个数大于N/2,假设这个段所表示的范围是q~q+n-1,那么这N个数的中位数就在q~q+n-1这个范围内,现在更改计数器的属性不在基于段而是基于每个数,且只针对q~q+n-1这n个数计数,还需要一轮扫面N个数来完成对n个计数器的确定,在本段之前的段计数器计数之和是t,t<2/N,那么只要依次从新的n个计数器开始累加到s,使得满足t+s>N/2那一个计数器,它所代表的数就是这N个数的中位数。

若有很大一组数据,数据的个数是N(每个数占4个字节),内存大小为M个字节,其中M<4*N,使得不能在现有内存情况下通过直接排序找到这N个数的中位数。解决海量数据中取中位数的方法有两种比较简单耗时的是用堆排序,还有一种是改造后基于段的计数:


1)分区间堆排序:

在现有M大小内存情况下若最多能够造出包含p个数据的堆,则先扫描一次这N个数据找到最小的p个数,耗时O(Nlog(p)),设这p个数中最大的数是a,将堆清空,在第二轮扫描出比a大的中最小的p个数,然后在把a改为记录这p个数中最大的数,依次类推,直到计算到某一轮p个数和之前够造出的数的个数大于N/2,在这p个数中找到所有数中的中位数,耗时的地方是每轮扫描构建堆都要用了O(Nlog(p)),构造的次数为N/(2*p),所以它的时间复杂度是O(N*N*log(p)/(2*p)).


2)基于段的计数


基于段的计数指的的是对一个区间范围的计数,与计数排序不同的是后者对每一个数出现次数的计数,而前者是对出现在某一区间范围内的数计数,段的大小取决于内存大小和每段计数器所占的字节数,而计数器大小又受总数据量有关,需满足计数器的最大计数能够大于最大个数N,比如本题中内存大小是M个字节,而N的大小至少需要用logN位来表示,设k个字节表示的无符号整数可以最大值大于N,则放在内存中计数器个数是M/k,设q=M/k,即这N个数分为q个段,每一段的大小是n=N/q,第一段数的范围是0~n-1,第二段是n~2*n-1,依此类推。现在我们可以从硬盘中逐个扫面这N个数,根据每个数的大小来修改他们对应范围的计数器,需要用O(N)时间完成对这q个段的计数。然后从第一段开始往后扫描累加每个段的计数器,直到累加到某一段的计数器使得它和这之前的个数大于N/2,假设这个段所表示的范围是q~q+n-1,那么这N个数的中位数就在q~q+n-1这个范围内,现在更改计数器的属性不在基于段而是基于每个数,且只针对q~q+n-1这n个数计数,还需要一轮扫面N个数来完成对n个计数器的确定,在本段之前的段计数器计数之和是t,t<2/N,那么只要依次从新的n个计数器开始累加到s,使得满足t+s>N/2那一个计数器,它所代表的数就是这N个数的中位数。