我问的是关于 Top K 算法的问题。我认为 O(n + k log n) 应该更快,因为好吧..例如,如果您尝试插入 k = 300 和 n = 100000000,我们可以看到 O(n + k log n)更小。
但是,当我使用 C++ 进行基准测试时,它显示 O (n log k) 的速度要快 2 倍以上。这是完整的基准测试程序:
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
#include <ctime>
#include <cstdlib>
using namespace std;
int RandomNumber () { return rand(); }
vector<int> find_topk(int arr[], int k, int n)
{
make_heap(arr, arr + n, greater<int>());
vector<int> result(k);
for (int i = 0; i < k; ++i)
{
result[i] = arr[0];
pop_heap(arr, arr + n - i, greater<int>());
}
return result;
}
vector<int> find_topk2(int arr[], int k, int n)
{
make_heap(arr, arr + k, less<int>());
for (int i = k; i < n; ++i)
{
if (arr[i] < arr[0])
{
pop_heap(arr, arr + k, less<int>());
arr[k - 1] = arr[i];
push_heap(arr, arr + k, less<int>());
}
}
vector<int> result(arr, arr + k);
return result;
}
int main()
{
const int n = 220000000;
const int k = 300;
srand (time(0));
int* arr = new int[n];
generate(arr, arr + n, RandomNumber);
// replace with topk or topk2
vector<int> result = find_topk2(arr, k, n);
copy(result.begin(), result.end(), ostream_iterator<int>(cout, "\n"));
return 0;
}
find_topk 的方法是在 O(n) 中构建一个大小为 n 的完整堆,然后将堆的顶部元素移除 k 次 O(log n)。find_topk2 的做法是构建一个大小为 k (O(k)) 的堆,使得 max 元素在顶部,然后从 k 到 n,比较看是否有任何元素小于顶部元素,如果是则 pop顶部元素,并推送新元素,这意味着 n 次 O(log k)。两种方法的编写方式都非常相似,因此我不相信任何实现细节(如创建临时对象等)会导致除了算法和数据集(随机)之外的差异。
我实际上可以分析基准测试的结果,并且可以看到 find_topk 实际上调用比较运算符的次数比 find_topk2 多得多。但我对理论复杂性的推理更感兴趣......所以两个问题。
- 忽略实现或基准,我期望 O(n + k log n) 应该比 O(n log k) 更好是错误的吗?如果我错了,请解释为什么以及如何推理以使我可以看到 O(n log k) 实际上更好。
- 如果我期望没有 1 没有错。那么为什么我的基准测试显示不是这样呢?