像BAT这种巨型互联网公司每天都要出来海量数据。假设从服务器上产生的数据条目数为n,这个值是事先不知道的,唯一确定的是这个值非常大,假定项目需要快速从这n条数据中查找第k小的条目,其中k的值是事先能确定的,请你设计一个设计一个满足需求并且兼顾时间和空间效率的算法。
这个题目的难度有若干处,第一是数据数n无法确定,你无法动态的分配合适的空间来存储数据。其次是数据条目数n相当大,如果直接根据n来分配内存会产生巨大的损耗,第三是速度要足够快,但要在海量级数据中实现快速查找不是一件容易的事情。
解决这道题的关键在于选取合适的数据结构。在前面的章节中,我们详细讲解过一种数据结构叫堆。回忆一下,这种数据结构有以下特点,第一,它是一只类似于二叉树的结构。第二,对于m个数据而言,建造一个堆的时间复杂度是O(m*lg(m)),第三,创建该数据结构不需要分配额外的内存,所以其空间复杂度是O(m),第四,一个关键的性质在于,如果创建的是大堆,那么m个元素中最大值就正好在根节点,如果是小堆,最小值就正好在根节点。第五,对堆插入一个元素或是删除一个元素,其时间复杂度是O(lg m).
我们以前曾经给出过完整的堆实现代码如下:
public class HeapPairSort {
private int heapSize = 0;
private Pair[] heapArray = null;
public HeapPairSort(Pair[] arr) {
heapSize = arr.length;
heapArray = arr;
}
private int parent(int i) {
return i/2;
}
private int left(int i) {
return i*2;
}
private int right(int i) {
return i*2+1;
}
private void maxHeapify(int i) {
//这里+1的原因是,数组下标是从0开始,而算法对数组下标是从1开始
i++;
int l = left(i);
int r = right(i);
//减1的原因是,数组元素的下标是由0起始的,而算法对数组下标的起始是由1开始
i--;
l--;
r--;
int largest = -1;
if (l < heapSize && heapArray[l].val > heapArray[i].val) {
largest = l;
} else {
largest = i;
}
if (r < heapSize && heapArray[r].val > heapArray[largest].val) {
largest = r;
}
if (largest != i) {
heapArray[largest].exchange(heapArray[i]);;
maxHeapify(largest);
}
}
public Pair[] buildMaxHeap() {
for (int i = heapSize / 2; i >= 0; i--) {
maxHeapify(i);
}
return heapArray;
}
public void heapSort() {
buildMaxHeap();
for (int i = heapArray.length - 1; i > 0; i--) {
//值最大的元素在根节点对应到数组就是值最大的元素在数组的起始位置
heapArray[i].exchange(heapArray[0]);
heapSize--;
maxHeapify(0);
}
}
public Pair maximun() {
return heapArray[0];
}
public Pair extractMax() {
if (heapSize < 1) {
return null;
}
Pair max = heapArray[0];
heapArray[0] = heapArray[heapSize - 1];
heapSize--;
maxHeapify(0);
return max;
}
public void increaseKey(int i, Pair k) {
if (heapArray[i].val > k.val) {
return;
}
heapArray[i] = k;
while (i > 0 && heapArray[parent(i)].val < heapArray[i].val) {
heapArray[parent(i)].exchange(heapArray[i]);
i = parent(i);
}
}
public Pair[] insert(Pair val) {
heapSize++;
Pair[] mem = new Pair[heapSize];
for (int i = 0; i < heapSize; i++) {
mem[i] = heapArray[i];
}
heapArray = mem;
Pair p = new Pair(Integer.MIN_VALUE, val.begin, val.end);
heapArray[heapSize - 1] = p;
increaseKey(heapSize - 1, val);
return heapArray;
}
}
上面代码构造的是一个大堆,也就是堆中节点最大值在根节点。由于我们要从事先不知道的n个元素中,查找到第k小的元素,其中k的值是确定的,那么我们可以构造一个含有k个元素的大堆,当有新的元素过来时,我们从大堆的根节点获得最大值,如果新来元素的值比根节点值小,那么我们将根节点从堆中去掉,将新节点插入到堆中,如果新来的元素值大于根节点,那么就直接忽略掉新元素,于是我们就可以始终保持所遇到的所有元素中排序在前k位的值,最后所有元素的访问完后,我们从堆的根节点处就可以得到海量数据元素中第k小的值了。
整个算法的时间复杂度是O(n*lg(k)).由于数值k是固定的,这相当与我们在O(n)的时间复杂度内完成了题目所给要求,由于堆的空间复杂度是O(k),因此空间复杂度也是线性的。我们看看主函数入口代码:
public class Search {
public static void main(String[] args) {
int n = 30;
int k = 17;
int[] array = new int[30];
int[] heapArray = new int [k];
Random rand = new Random(100);
HeapSort hs = null;
for (int i = 0; i < n; i++) {
array[i] = Math.abs(rand.nextInt() % 100);
//用前k个元素构建大堆
if (i <= k - 1) {
heapArray[i] = array[i];
}
}
hs = new HeapSort(heapArray);
hs.buildMaxHeap();
System.out.println("max is " + hs.maximun());
//将第k个以后的元素插入大堆,让大堆记录前k大的元素
for (int i = k; i < n; i++) {
//如果当前元素比根元素要小就将其加入堆
if (hs.maximun() > array[i]) {
System.out.println("heap max is:"+hs.maximun() +" array element is :" + array[i] );
hs.extractMax();
hs.insert(array[i]);
}
}
System.out.println("The kth element is :" + hs.maximun());
Arrays.sort(array);
System.out.println("The kth element in array is:" + array[k-1]);
}
}
代码用一个含有30个元素的数组array来模拟题目中的海量数据条目,因此n=30,我们想从30个未知数值中找到第17小的数,于是在代码中又构造了一个只包含17个元素的大堆。在下面的for循环中,代码判断新来的元素是否比大堆根节点元素要小,如果是的话就把根节点去掉,将新元素加入大堆。无论有多少元素过来,大堆始终保持17个元素,当所有元素都处理过后,大堆的根节点就是指定第k小的元素。
代码中动态分配了一个数组heapArray,但由于k是预先知道的固定值,是一个常量,因此即使代码有动态内存分配,我们也认为这段内存的大小是O(1)。上面代码运行结果如下:
根据输出结果,数组array的第17小的元素值是50,我们从大堆中拿到的根节点也是50,由此可见,算法及其代码实现是正确的。
更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
网友评论