这道题最简单的思路是排序,时间复杂度是O(nlog(n))。但是这样做在那n-k 个数的排序上浪费了资源。
改进一下,将数组的前k个数作为最小的k数的缓存。从第k+1个数开始遍历,如果有比前k个数小的,就将其和前k个数那个较大交换。
照这个思路,可以引入一个结构,使得前k个数总是最大的数在第一个,这样每次遇到一个数值需要和前k个数中排在第一位的那个最大数比较就可以了。
这个结构就是最大堆。
思路一:维护一个maxSize为k的最大堆,用来存放这k个数,遍历数组,如果堆未满,插入堆中。如果堆已满,如果数字比堆的根节点小,则删除堆的根节点,向堆中插入这个数字。
时间复杂度为 O(nlog(k))。如果求最大的k个数,就是使用最小堆了。
思路二:其实我们也可以在n个整数的范围内构建最小堆,然后每次将堆顶的最小值取走,然后调整,再取走堆顶值,取k次,自然就找到了最小的k个数。
这样做的时间复杂度是多少呢?基于n个无序数构建最小堆,时间是O(n),而取走堆顶元素,将堆末元素放到堆顶重新调整的时间复杂度是 O(h),h为堆高度,这里堆高度h总是logn保持不变。
因此时间复杂度是O(n+klogn),这个思路可以再改进:每次重新调整,只需要基于堆顶部k层进行调整,堆末元素被放到堆顶后,最多只需要下调至k层即可,因此调整的时间复杂度成了O(k)。这样做的时间复杂度成了 O(n+kk)。
可以证明O(n+k*k) < O(nlog(k)),但是实际情况下,是否思路二更好呢?
我们别忘了思路二的初始化需要基于整个n个数构建堆,而思路一则不需要。实际情况下,我们往往需要基于大量分布式存储的n个数找出k个数,例如:在分布式存在10台服务器上的总大小大约2T的访问页面记录中找出访问量最高的100个页面。想要跨10台服务器整体构建最大堆,显然不现实,而思路一则只需要维护一个100的最小堆,然后顺序遍历10台服务器上的记录即可。
因此思路一更加具有实际意义。
这里给出思路一的实现。
如果真正写代码,要知道堆是没有STL的,也就是说我们要自己实现。
书中使用了multiset,multiset和set一样,都是基于红黑树实现,区别是set不允许重复而multiset允许重复。
那么multiset和vector区别在哪里?区别在于multiset支持排序。multiset的插入和删除的时间复杂度也都为 O(logk)
代码:
[](javascript:void(0)?
typedef multiset > intSet;
typedef multiset >::iterator setIterator;
void GetLeastNumbers_Solution2(const vector& data, intSet& leastNumbers, int k)
{
leastNumbers.clear();
if(k < 1 || data.size() < k)
return;
vector::const_iterator iter = data.begin();
for(; iter != data.end(); ++ iter)
{
if((leastNumbers.size()) < k)
leastNumbers.insert(*iter);
else
{
setIterator iterGreatest = leastNumbers.begin();
if(*iter < *(leastNumbers.begin()))
{
leastNumbers.erase(iterGreatest);
leastNumbers.insert(*iter);
}
}
}
}
[](javascript:void(0)?
第三种思路,快速选择 法。
先选取一个数作为基准比较数(作者称为“枢纽元”,即pivot),用快排方法把数据分为两部分Sa和Sb。
如果K< |Sa|( |Sa|表示Sa的大小),则对Sa部分用同样的方法继续操作;
如果K= |Sa|,则Sa是所求的数;
如果K= |Sa| + 1,则Sa和这个pivot一起构成所求解;
如果K> |Sa| + 1,则对Sb部分用同样的方法查找最小的(K- |Sa|-1)个数(其中Sa和pivot已经是解的一部分了)。
当pivot选择的足够好的时候,可以做到时间复杂度是O(n)
那么如何选择一个好的pivot?
这里必须提BFPRT算法,这个算法就是为了寻找数组中第k小的数而设。
BFPRT是一种获得较优秀pivot的方法。其过程有一个flash动画作为演示。其过程是将n个数5个一组划分,求出没一个5元组的中位数,然后再基于这些中位数继续求中位数,这个重复的次数应该是由k的大小来定,随后将选出的中位数作为pivot,将小于pivot的数交换到数组的左侧。接着基于这些小于pivot的值,继续通过“寻找中位数,定pivot,交换法” 来缩小范围,直到最后在一个较小范围内找到k个最小值。
BFPRT算法的时间复杂度做到了O(n)。这个算法的原文链接在此:Time Bounds for Selection。
vector GetLeastNumbers_Solution(vector input, int k) {
vector output;
if (input.empty() || k == 0||k>input.size())
return ;
int start = 0;
int end = input.size() - 1;
int index = partition(input, start, end);
while (index != k - 1){
if (index > k - 1){
end = index - 1;
index = partition(input, start, end);
}
else{
start = index + 1;
index = partition(input, start, end);
}
}
for (int i = 0; i < k; ++i)
output.push_back(input[i]);
return output;
}
int partition(vector &array, int start, int end) {
// 选择一个pivotIndex
int pivotIndex = rand() % (end - start + 1) + start; // 随机选一个
swap(array[pivotIndex], array[end]); // 将pivot换到末尾
int small = start;
for (int i = start; i < end; i++) {
if (array[i] < array[end]) {
swap(array[small], array[i]);
small++;
}
}
swap(array[small], array[end]);
return small;
}