题目描述
一亿个数找最大的1000个数,要求效率高占用内存少
思路
思路1
首先想到的是使用快速排序先将元素排好序,再取出最大的那一部分1000个数,由于快速排序的时间复杂度为o(nlog2n),也就是(10^ 8)× log2(10^ 8),约等于(10^ 8)× 27,这也是非常大的操作量了。由于快速排序是原地排序,仅需要一个额外的元素内存空间,所以空间复杂度为o(1),满足占用内存少。
思路2
可以先取出前1000个元素存放到一个临时的容器tmp中,从1001个元素开始逐个与tmp中的最小值min进行比较,如果大于tmp中最小值min,则将该元素交换进tmp中,重新寻找tmp的min,否则与下一个元素。
用简单的方法(先不考虑在此处使用堆的办法)在tmp中找最小值的时间复杂度为o(1000),后面每次交换后都要重新寻找最小值,最坏的情况下(前1000个是100000000个元素中最小的部分)需要时间(10^ 8 - 1000)× 1000,约等于10^ 11,时间复杂度很大,空间复杂度o(1001)也很大
思路3
结合思路1和思路2的方法,可以使用堆排序(本题恰恰是堆排序最合适的场合),由于堆排序构造堆和每一次重建堆都会将值最大的元素放在第一个位置,所以只需要构造一次+重建999次就可以得到最大的1000个数。首先将数据集合构造成堆(自下向上构造堆的时间复杂度为o(n)),将最大值first与末尾数last交换位置,然后再对[first, last - 1]重建堆999次(自顶向下重建堆的时间复杂度为o(2log2n)),所以总的时间复杂度为n + 999 × log2n = (10^ 8) + 2 × 999 × 27,约等于10^ 8,空间复杂度为o(1),效率高且占用内存少
tip:push_heap(自下向上)复杂度为O(logN),pop_heap(自顶向下)复杂度为O(2logN),虽然是常数项的区别。
原因:push_heap是把数字加到末尾,并不断上溯。每次上溯时它只和其父节点比较,所以是O(logN)。
pop_heap把原来的数组末尾元素放到堆顶,并不断下溯。每次下溯时它会和其两个子节点比较,所以是O(2logN)。
代码实现
C++ STL自带堆的数据结构,只能对支持随机访问的序列容器(array、vector)进行操作。此处数组用array容器来构建,方便使用模板。
make_heap构造堆
push_heap在堆末尾添加新元素
pop_heap交换堆顶和堆尾元素,并对[first, last - 1]元素重建堆
/*
一亿个数找最大的1000个数,要求效率高占用内存少。
*/
#include <iostream>
#include <array>
#include <algorithm>
using namespace std;
const int NUM_SET_SIZE = 1000000; // 由于笔记本跑不了一亿个数,改成1000000
const int MAX_AMOUNT = 1000;
inline void show(array<int, NUM_SET_SIZE> ob, int len)
{
auto riter = ob.rbegin();
for (int i = 0; i < 20; i++)
cout << *riter++ << endl;
}
int main()
{
array<int, NUM_SET_SIZE> num_set;
// 利用递增的方法为每个元素赋值
int i = 0;
for (auto &var : num_set)
var = ++i;
random_shuffle(num_set.begin(), num_set.end()); // 将元素顺序打乱,形成无序序列
// 显示后20个元素的值,验证是否乱序
show(num_set, 20);
make_heap(num_set.begin(), num_set.end()); // 构造成一个堆
for (int i = 0; i < MAX_AMOUNT; i++)
// 将last - 1处的值与first处进行交换,并使[first, last - 2]处成为一个有效堆
pop_heap(num_set.begin(), num_set.end() - i);
// 显示后20个元素的值,验证是否已得到最大值
show(num_set, 20);
return 0;
}