问题描述:从arr[1]到arr[n]这n个数中,找出其中最大的k个数。
最容易想到的方法当然是直接排序,将n个数排序之后,取出最大的前k个,即为所得。最快的排序算法的时间复杂度一般为O(NlogN),如快速排序。伪代码如下:
sorted(arr, 1, n);
return arr[1, k];
但是,同样存在以下问题:
我们借鉴快速排序的思想。
先简单介绍一下快速排序:快速排序基本思想是随机选取一个数作为关键字,经过一趟排序,将整段序列分为两个部分,其中一部分的值都小于关键字,另一部分都大于关键字。然后继续对这两部分继续进行排序,从而使整个序列达到有序。
快排代码如下(C语言):
void QuickSort(int* arr, int left, int right){
int mid = 0;
if(left >= right){
return;
}
mid = Partition(arr, left, right);
QuickSort(arr, left, mid - 1);
QuickSort(arr, mid + 1, right);
}
int Partition(int* arr, int left, int right){
int key;
int start = left;
key = arr[left];
while(left < right){
while(left < right && arr[right] >= key)
{
right--;
}
while(left < right && arr[left] <= key)
{
left++;
}
Swap(&arr[left], &arr[right]);
}
Swap(&arr[left], &arr[start]);
return left;
}
我们借鉴快排的思想,在数组中随便找一个元素key,把数组分为arr_a, arr_b两部分,其中a组中的元素大于等于key,b组的元素小于k。那么:
int findTopK(int* arr, int left, int right, int k){
int index = -1;
if(left < right){
int pos = Partition(array, left, right);
int len = pos - left + 1;
if(len == k){
index = pos;
}
else if(len < k){//a中元素个数小于K,到b中查找k-len个数字
index = findTopK(array, pos + 1, right, k - len);
}
else{//a组中的元素个数大于等于k
index = findTopK(array, left, pos - 1, k);
}
}
return index;
}
那么可不可以不排序也能找出前K个数呢?
我们考虑用容量为K的小顶堆堆来存储最大的K个数。
先用前k个元素生成一个小顶堆,这个小顶堆用于存储,当前最大的k个元素。接着,从第k+1个元素开始扫描,和堆顶(堆中最小的元素)比较,如果被扫描的元素大于堆顶,则替换堆顶的元素,并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。直到,扫描完所有n-k个元素,最终堆中的k个元素即为所求。
这种方法当数据量比较大的时候,比较方便。因为对所有的数据只会遍历一次。
Java实现如下:
public class MinHeap {
// 堆的存储结构为数组
private int[] data;
// 将一个数组传入构造方法,并转换成一个小顶堆
public MinHeap(int[] data) {
this.data = data;
buildHeap();
}
// 将数组转换成最小堆
private void buildHeap() {
// 完全二叉树只有数组下标小于或等于 (data.length) / 2 - 1 的元素有孩子结点,遍历这些结点。
for (int i = (data.length) / 2 - 1; i >= 0; i--) {
// 对有孩子结点的元素heapify
heapify(i);
}
}
private void heapify(int i) {
// 获取左右结点的数组下标
int l = left(i);
int r = right(i);
// 这是一个临时变量,表示 跟结点、左结点、右结点中最小的值的结点的下标
int smallest = i;
// 存在左结点,且左结点的值小于根结点的值
if (l < data.length && data[l] < data[i])
smallest = l;
// 存在右结点,且右结点的值小于以上比较的较小值
if (r < data.length && data[r] < data[smallest])
smallest = r;
// 左右结点的值都大于根节点,直接return,不做任何操作
if (i == smallest)
return;
// 交换根节点和左右结点中最小的那个值,把根节点的值替换下去
swap(i, smallest);
// 由于替换后左右子树会被影响,所以要对受影响的子树再进行heapify
heapify(smallest);
}
// 获取右结点的数组下标
private int right(int i) {
return (i + 1) << 1;
}
// 获取左结点的数组下标
private int left(int i) {
return ((i + 1) << 1) - 1;
}
// 交换元素位置
private void swap(int i, int j) {
int tmp = data[i];
data[i] = data[j];
data[j] = tmp;
}
// 获取对中的最小的元素,根元素
public int getRoot() {
return data[0];
}
// 替换根元素,并重新heapify
public void setRoot(int root) {
data[0] = root;
heapify(0);
}
}
public static int[] topK(int[] a, int k) {
if (a == null || k >= a.length) {
return a;
}
// 先取K个元素放入一个数组topK中
int[] topK = new int[k];
for (int i = 0; i < k; i++) {
topK[i] = a[i];
}
// 转换成最小堆
MinHeap heap = new MinHeap(topK);
// 从k开始,遍历data
for (int i = k; i < a.length; i++) {
int root = heap.getRoot();
// 当数据小于堆中最小的数(根节点)时,替换堆中的根节点,再转换成堆
if (a[i] < root) {
heap.setRoot(a[i]);
}
}
return topK;
}
首先查找 max 和 min,然后计算出mid = (max + min) / 2。实质是寻找最大的K个数中最小的一个。
但是这种方法在实际应用中效果不佳,所以此处不再贴代码。
先通过Hash法,把海量数据中的数字去重,如果重复率较高,这样会减少很大的内存用量,从而缩小运算空间,然后通过上面的第二或者第三种方法计算TopK。