【海量数据算法】如何在10亿数中找出前100大的数(TopN)
一、题目
在10亿数字的文件中找到最大的前100个数字。
二、分析
2.1 排序方法
快速选择(Quick Select
)和快速排序(Quick Sort
)两个算法的作者都是Hoare
,并且思想也非常接近:选取一个基准元素pivot
,将数组切分(partition
)为两个子数组,比pivot
大的扔左子数组,比pivot
小的扔右子数组,然后递推地切分子数组。Quick Select
不同于Quick Sort
之处在于其没有对每个子数组做切分,而是对目标子数组做切分。其次,Quick Select
与Quick Sort
一样,是一个不稳定的算法;pivot
选取直接影响了算法的好坏,最坏情况下的时间复杂度达到了O(n2)
。
Quick Select
的Java
实现如下:
public static long quickSelect(long[] nums, int start, int end, int k) {
if (start == end) {
return nums[start];
}
int left = start;
int right = end;
long pivot = nums[(start + end) / 2];
while (left <= right) {
while (left <= right && nums[left] > pivot) {
left++;
}
while (left <= right && nums[right] < pivot) {
right--;
}
if (left <= right) {
long temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
if (start + k - 1 <= right) {
return quickSelect(nums, start, right, k);
}
if (start + k - 1 >= left) {
return quickSelect(nums, left, end, k - (left - start));
}
return nums[right + 1];
}
根据快速排序划分的思想
(1) 递归对全部数据分红[a,b)b(b,d]两个区间,(b,d]区间内的数都是大于[a,b)区间内的数
(2) 对(b,d]重复(1)操做,直到最右边的区间个数小于100个。注意[a,b)区间不用划分
(3) 返回上一个区间,并返回此区间的数字数目。接着方法仍然是对上一区间的左边进行划分,分为[a2,b2)b2(b2,d2]两个区间,取(b2,d2]区间。若是个数不够,继续(3)操做,若是个数超过100的就重复1操做,直到最后右边只有100个数为止。
实例代码
public class QuickSelect {
private static int len = 10;
/**
* 多次调用快速排序,讲数据中最大的10位数移动至最右边
*
* @param array
* @param left
* @param right
*/
public static void quickSort(int[] array, int left, int right) {
int pivot;
if (left < right) {
pivot = partition(array, left, right);//取出枢轴
//如果pivot右边的输大于10个,再次重复操作(1)
if (right - pivot + 1 > len) {
quickSort(array, pivot + 1, right);
} else if (right - pivot + 1 < len) {//pivot右边的数小于10个,执行操作(2)
len = len - (right - pivot + 1);
quickSort(array, left, pivot + 1);
}
}
}
/**
* pivotValue作为枢轴,较之小的元素排序后在其左,较之大的元素排序后在其右
*
* @param array
* @param left
* @param right
* @return
*/
public static int partition(int[] array, int left, int right) {
int pivot = array[left];
while (left < right) {
while (left < right && array[right] >= pivot) {
--right;
}
//讲比枢轴小的元素移到低端,此时right位相当于空,等待地位比pivot大的数补上
array[left] = array[right];
while (left < right && array[left] <= pivot) {
++left;
}
//将比枢轴大的元素移到高端,此时left位相当于空,等待高位比pivot小的数补上
array[right] = array[left];
}
//当left == right,完成一趟快速跑排序,此时left位相当于空,等待pivotkey补上
array[left] = pivot;
return left;
}
public static void main(String[] args) {
int[] array = new int[100];
for (int i = 0; i < array.length; i++) {
array[i] = (int)(Math.random() * 1000);
}
print(array);
quickSort(array,0,array.length -1);
for(int i=array.length-10;i<array.length;i++){
System.out.print(array[i] + " ");
}
System.out.println("-----------------");
print(array);
}
public static void print(int[] data) {
for(int i = 0; i < data.length; i++) {
System.out.print(data[i] + " ");
}
System.out.println();
}
}
2.2 堆排序法
针对一般的top K
问题,一般都会默认K
很小,所以一般的top K
问题,可以选择使用堆来解决。
堆有个重要的性质:每个结点的值均不大于其左右孩子结点的值,则堆顶元素即为整个堆的最小值。JDK
中PriorityQueue
实现了堆这个数据结构堆,通过指定comparator
字段来表示小顶堆或大顶堆,默认为自然序(natural ordering
)。
小顶堆解决Top K
问题的思路:小顶堆维护当前扫描到的最大K
个数,其后每一次扫描到的元素,若大于堆顶则入堆,然后删除堆顶;依此往复,直至扫描完所有元素。
Java
实现第K
大整数代码如下:
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> minQueue = new PriorityQueue<>(k);
for (int num : nums) {
if (minQueue.size() < k || num > minQueue.peek())
minQueue.offer(num);
if (minQueue.size() > k)
minQueue.poll();
}
return minQueue.peek();
}
求第K
大的数,这里没有说明K
的范围,那么最坏情况下,K == N/2
,无论维护一个top K
的小顶堆还是维护一个top(N - K)
的大顶堆,都需要占用O(N/2)
的内存,而对于海量数据而言,这显示是一笔非常大的开销。
手动实现堆
先取出前100
个数,维护一个100
个数的最小堆,遍历一遍剩余的元素,在此过程当中维护堆就能够了。具体步骤以下:
step1
:取前m
个元素(例如m=100
),创建一个小顶堆。保持一个小顶堆得性质的步骤,运行时间为O(lgm)
;创建一个小顶堆运行时间为m*O(lgm)=O(m lgm)
;
step2
:顺序读取后续元素,直到结束。每次读取一个元素,若是该元素比堆顶元素小,直接丢弃
若是大于堆顶元素,则用该元素替换堆顶元素,而后保持最小堆性质。最坏状况是每次都须要替换掉堆顶的最小元素,所以须要维护堆的代价为(N-m)*O(lgm)
;
最后这个堆中的元素就是前最大的10W
个。时间复杂度为O(N lgm)
。
补充:这个方法的说法也能够更简化一些:
假设数组arr
保存100
个数字,首先取前100
个数字放入数组arr
,对于第101
个数字k
,若是k
大于arr
中的最小数,则用k
替换最小数,对剩下的数字都进行这种处理。
实例代码
public class TopN {
// 父节点
private int parent(int n) {
return (n - 1) / 2;
}
// 左子节点
private int left(int n) {
return 2 * n + 1;
}
// 右子节点
private int right(int n) {
return 2 * n + 2;
}
// 构建堆
private void buildHeap(int n, int[] data) {
for(int i = 1; i < n; i++) {
int t = i;
// 调整堆
while(t != 0 && data[parent(t)] > data[t]) {
int temp = data[t];
data[t] = data[parent(t)];
data[parent(t)] = temp;
t = parent(t);
}
}
}
// 调整data[i]
private void adjust(int i, int n, int[] data) {
if(data[i] <= data[0]) {
return;
}
// 置换堆顶
int temp = data[i];
data[i] = data[0];
data[0] = temp;
// 调整堆顶
int t = 0;
while( (left(t) < n && data[t] > data[left(t)])
|| (right(t) < n && data[t] > data[right(t)]) ) {
if(right(t) < n && data[right(t)] < data[left(t)]) {
// 右子节点更小,置换右子节点
temp = data[t];
data[t] = data[right(t)];
data[right(t)] = temp;
t = right(t);
} else {
// 否则置换左子节点
temp = data[t];
data[t] = data[left(t)];
data[left(t)] = temp;
t = left(t);
}
}
}
// 寻找topN,该方法改变data,将topN排到最前面
public void findTopN(int n, int[] data) {
// 先构建n个数的小顶堆
buildHeap(n, data);
// n往后的数进行调整
for(int i = n; i < data.length; i++) {
adjust(i, n, data);
}
}
// 打印数组
public void print(int[] data) {
for(int i = 0; i < data.length; i++) {
System.out.print(data[i] + " ");
}
System.out.println();
}
}
2.3 分治法
先把10
亿个数分成100
份,每份1000w
个数,然后在1000w
个数中分别找出最大的100
个数,最后在100*100
个数中找出最大的100
个。这里我想可以用分布式的处理,多台主机才会更快。
具体代码可以参考10亿int型数,统计只出现一次的数