前K个最小选择算法-O(n + k log n)vs O(n log k),k <&lt;&lt; ñ

时间:2011-07-12 19:21:35

标签: c++ algorithm complexity-theory asymptotic-complexity

关于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,比较以查看是否有任何元素小于top元素,如果是,则弹出顶部元素,并推送新元素,这意味着n次O(log k)。 两种方法都写得非常相似,所以我不相信任何实现细节(比如创建临时工具等)会导致算法和数据集(随机)之间的差异。

我实际上可以分析基准测试的结果,并且可以看到find_topk实际上比find_topk2更多次调用比较运算符。但我对理论复杂性的推理更感兴趣......所以有两个问题。

  1. 无视实施或基准,我错误地认为O(n + k log n)应该优于O(n log k)?如果我错了,请解释为什么以及如何推理我可以看到O(n log k)实际上更好。
  2. 如果我没有错,期望没有。那么为什么我的基准显示不然?

3 个答案:

答案 0 :(得分:4)

几个变量中的大O是复杂的,因为您需要假设变量如何相互缩放,因此您可以明确地将限制变为无穷大。

如果例如。 k~n ^(1/2),则O(n log k)变为O(n log n),O(n + k log n)变为O(n + n ^(1/2)log n)= O (n),哪个更好。

如果k~log n,那么O(n log k)= O(n log log n)和O(n + k log n)= O(n),这是更好的。请注意,log log 2 ^ 1024 = 10,因此对于任何实际的n,隐藏在O(n)中的常量可能大于log log n。

如果k =常数,那么O(n log k)= O(n)和O(n + k log n)= O(n),这是相同的。

但常量起着重要的作用:例如,构建堆可能涉及读取数组3次,而构建长度为k的优先级队列只需要一次通过数组,并且需要一个小的常量时间日志k用于查找。

哪个“更好”是不清楚的,尽管我的快速分析倾向于表明O(n + k log n)在k的温和假设下表现更好。

例如,如果k是一个非常小的常数(比如k = 3),那么我已经准备好打赌make_heap方法在真实世界数据上的性能优于优先级队列。

明智地使用渐近分析,最重要的是,在得出结论之前对代码进行分析。

答案 1 :(得分:1)

您正在比较两个最坏情况的上限。对于第一种方法,最坏的情况几乎与平均情况相同。对于第二种情况,如果输入是随机的,那么当你将多个项目传递到堆中时,有可能立即丢弃新值,因为它不会替换任何顶部K是相当高,所以最坏的情况估计是悲观的。

如果您要比较挂钟时间而不是比较,您可能会发现基于堆的算法具有较大的堆积,因为它们具有可怕的存储局部性而不会赢得许多比赛 - 并且现代微处理器上的常数因素受到什么级别的影响很大你最终工作的内存 - 发现你的数据是在真实的内存芯片中(或者更糟糕的是,在磁盘上),而不是某种程度的缓存会减慢你的速度 - 这是一种耻辱,因为我真的很喜欢heapsort。

答案 2 :(得分:0)

请记住,您现在可以使用std :: nth_element而不必使用堆并自行执行操作。由于默认的比较运算符是std :: less&lt;&gt;(),你可以这样说:

std :: nth_element(myList.begin(),myList.begin()+ k,myList.end());

现在,从0到k位置的myList将是最小的k个元素。