这些天看到了一道题,是一道比较出名的面试题,题目字面上比较简单。
输入若干个float数字(百万级以上) ,编写一个算法从中取出指定数量(100个以内)的最大的数字。
我们先分析一下这道题,从一堆数字里取出几个最大的数,以我们通常的思想去考虑,首先想到的是对这堆数字进行倒序排序,取出前几个就是我们要的结果,这样实现是没错的。可是注意看括号中的注释,输入的数字量级是百万级以上的,如果单纯为了实现结果,排序是没错的,但是对百万级的数据进行排序,不管使用任何排序都会花费很多时间。并且float在java中占4个字节,1000000*4/1024/1024=3.8 G,这样庞大的数据同样需要考虑到空间复杂度的问题,排序算法基本都需要额外的空间,如果对这样大的数据进行排序是不可能的。
程序员在很多人的印象中都是呆板,两眼无神,目光呆滞(也不乏有特别帅气的),可我们的脑子是很灵活的。我们需要转变一下思路,从需要取出数字中下手。
我们基本确定一下我们的思路,我们可以指定一个额外的数组,这个数组就是我们需要取出的数字,因为这个数组的量级很小(100以内),我们每次去维护这个小量级的数组肯定比直接排序大量级的数组好,所以我们遍历百万级的数组去和这个数组比较,如果比这个数组中最小的数字大,我们就替换它,最终会得到一组最大数字。
由于取出的数字是最大的,所以我们首先想到的是选择排序,选择排序就是每次都会取出最大或者最小的数字,这看起来有点符合我们的需求。我们先考虑到了直接选择排序,直接选择排序需要对数组的所有元素进行两两比较,时间复杂度为O(n^2),由于选择排序都是就地排序的算法,不需要额外的空间,所以空间复杂度是满足的。堆排序也属于选择排序,本文使用堆排序来实现。
我们先需要熟悉一下堆排序,堆其实就是一个完全二叉树,满足中序遍历的顺序,堆排序根据小根堆(大根堆)的性质进行排序,满足二叉树中的任何子树都为父节点最小(最大),每当堆中数字改变都需要去维护这个性质
逻辑图 数组结构
我们指定获取前M个最大的数字,所以我们构建一有M个节点的小根堆,遍历百万级数组,用数组去和小根堆的根节点进行比较,如果比根节点大,则替换根节点,然后重建小根堆,维护小根堆的性质,保证根节点在堆中最小,遍历完成后,堆中数字则为数组中最大数字。下面上代码!!
/**
* Created by zym on 2018/3/7.
*/
/**
* 思路:
* 构建一个包含M个节点的小根堆,之后循环数组,用数组的元素和小根堆的根作比较
* 如果比根大,则替换根,重新维护小根堆,直到循环完毕数组,剩下的为小根堆
* 这样不需要对整个数组进行堆排序,每次只需要维护一个100节点以内的堆,提升效率
*/
public class HeapUtils {
/**
* 替换元素
* @param data 指定数组
* @param i 位置1
* @param j 位置2
*/
private static void replace(float[] data, int i, int j) {
if (i == j) {
return;
}
float temp=data[i];
data[i]=data[j];
data[j]=temp;
}
/**
* 小根堆排序
* 利用小根堆的性质,最终的根节点为树中节点的最小值,将最小值放在最后
* 剩下的节点继续进行上述操作,直至倒数第二个节点,最后一个节点为最大节点无需判断
* @param data 指定数组
* @return
*/
public static float[] heapSort(float[] data) {
for (int i = 0; i < data.length-1; i++) {
createMindHeap(data, data.length - 1 - i);
//每次都将最小的数字放到最后,然后用剩下的数组继续排序
replace(data, 0, data.length - 1 - i);
// print(data);
}
return data;
}
/**
* 当前节点 i==0? null:(i-1)/2
* 左孩子节点 2*i+1
* 右孩子节点2*i+2
*
* 创建小根堆,只保证树的根为小根堆
* @param data 指定数组
* @param lastIndex 最后一个节点位置
*/
private static void createMindHeap(float[] data, int lastIndex) {
//从最后一个节点作为左节点的树开始
for (int i = (lastIndex - 1) / 2; i >= 0; i--) {
// 记录当前节点,根据当前节点进行判断操作
int k = i;
// 判断左子节点是否存在,如果小于等于最大节点数则存在
/**
* 这里使用while循环是为了保证每个子树的根节点为小根堆
* 如果使用if只能保证最终根为最小根
*/
while (2 * k + 1 <= lastIndex) {
//暂且定为最小的为左子节点
int smallIndex = 2 * k + 1;
if (smallIndex+1 <= lastIndex) {
//如果右子节点存在,判断左右大小(不保证左右节点满足小根堆的性质,因为只需使最小节点为根节点)
if (data[smallIndex] > data[smallIndex + 1]) {
// 若右子节点值比左子节点值小,则samllIndex记录的是右子节点的值
smallIndex++;
}
}
if (data[k] > data[smallIndex]) {
// 若当前节点值比子节点最小值大,则交换2者得值,交换后将smallIndex值赋值给k
replace(data, k, smallIndex);
//标记当前节点为最小节点,继续判断此节点作为根的堆是否为小根堆
k = smallIndex;
} else {
break;
}
}
}
}
/**
* 获得指定数量的最大值数组
* @param count 指定数量
* @param data 指定数组
* @return
*/
public static float[] getMaxNumber(int count,float []data){
float[] maxNubmerArr = new float[count];
for(int i=0;i<data.length;i++){
if(data[i]>maxNubmerArr[0]){
maxNubmerArr[0]=data[i];
createMindHeap(maxNubmerArr,count-1);
//查看每次取得的数字
// print(maxNubmerArr);
}
}
return maxNubmerArr;
}
/**
* 输出数组
* @param data
*/
public static void print(float[] data) {
for (int i = 0; i < data.length; i++) {
System.out.print(data[i] + "\t");
}
System.out.println();
}
}
测试代码!!!!
/**
* Created by zym on 2018/3/7.
*/
public class TestDemo {
@Test
public void test1(){
float arr[]= new float[1000000];
for (int i=0;i<1000000;i++){
Random random=new Random();
float v = random.nextFloat() * 50f;
arr[i]=v;
}
long start=System.currentTimeMillis();
float[] maxNumber = HeapUtils.getMaxNumber(100, arr);
HeapUtils.heapSort(maxNumber);
HeapUtils.print(maxNumber);
System.out.println(System.currentTimeMillis()-start);
}
}
执行时间为10ms