私はトップ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))のヒープを構築し、次にkからnまで、要素が一番上の要素よりも小さいかどうかを比較し、そうである場合はポップすることです。一番上の要素を押して、新しい要素をプッシュします。これは、O(log k)のn倍を意味します。どちらのアプローチもまったく同じように記述されているため、実装の詳細(一時的なものの作成など)によって、アルゴとデータセット(ランダム)以外に違いが生じる可能性はないと思います。
ベンチマークの結果を実際にプロファイリングすることができ、find_topkが実際にfind_topk2よりも何度も比較演算子を呼び出していることがわかりました。しかし、私は理論的な複雑さの推論にもっと興味があります。2つの質問です。
- 実装またはベンチマークを無視して、O(n + k log n)がO(n log k)よりも優れていると期待するのは間違っていましたか?私が間違っている場合は、O(n log k)が実際に優れていることがわかるように理由と理由を説明してください。
- 私が1を期待するのが間違っていないのなら、なぜ私のベンチマークはそうではないのですか?