【问题标题】:What makes this bucket sort function slow?是什么让这个桶排序功能变慢了?
【发布时间】:2011-04-26 14:34:11
【问题描述】:

函数定义为

void bucketsort(Array& A){
  size_t numBuckets=A.size();
  iarray<List> buckets(numBuckets);

  //put in buckets
  for(size_t i=0;i!=A.size();i++){
    buckets[int(numBuckets*A[i])].push_back(A[i]);
  }

  ////get back from buckets
  //for(size_t i=0,head=0;i!=numBuckets;i++){
  //size_t bucket_size=buckets[i].size();
  //for(size_t j=0;j!=bucket_size;j++){
  //  A[head+j] = buckets[i].front();
  //  buckets[i].pop_front();
  //}
  //head += bucket_size;
  //}
 for(size_t i=0,head=0;i!=numBuckets;i++){
   while(!buckets[i].empty()){
     A[head]          = buckets[i].back();
     buckets[i].pop_back();
     head++;
   }
 }

  //inseration sort
  insertionsort(A);
}

List 在 STL 中只是 list&lt;double&gt;

数组的内容是在[0,1)中随机生成的。理论上桶排序应该比快速排序更快,因为它是O(n),但是它失败了,如下图。

我使用google-perftools 在 10000000 双数组上对其进行分析。它报告如下

似乎我不应该使用 STL 列表,但我想知道为什么? std_List_node_base_M_hook 是做什么的?我应该自己写列表类吗?

PS:实验与改进
我试过只留下放入桶的代码,这解释了大部分时间都用于建立桶。
进行了以下改进: - 使用STL向量作为桶,为桶预留合理空间 - 使用两个辅助数组来存储构建桶的信息,从而避免使用链表,如下代码

void bucketsort2(Array& A){
  size_t    numBuckets = ceil(A.size()/1000);
  Array B(A.size());
  IndexArray    head(numBuckets+1,0),offset(numBuckets,0);//extra end of head is used to avoid checking of i == A.size()-1

  for(size_t i=0;i!=A.size();i++){
    head[int(numBuckets*A[i])+1]++;//Note the +1
  }
  for(size_t i=2;i<numBuckets;i++){//head[1] is right already
    head[i] += head[i-1];
  }

  for(size_t i=0;i<A.size();i++){
    size_t  bucket_num         = int(numBuckets*A[i]);
    B[head[bucket_num]+offset[bucket_num]] = A[i];
    offset[bucket_num]++;
  }
  A.swap(B);

  //insertionsort(A);
  for(size_t i=0;i<numBuckets;i++)
    quicksort_range(A,head[i],head[i]+offset[i]);
}

下图中的结果 其中行从使用列表作为存储桶的列表开始,从使用向量作为存储桶的向量开始,使用辅助数组开始 2。默认情况下最后使用插入排序,有些使用快速排序,因为存储桶大小很大。
注意“list”和“list,only put in”、“vector,reserve 8”和“vector,reserve 2”几乎重叠。
我会尝试保留足够内存的小尺寸。

【问题讨论】:

  • O-bounds 是渐近定义的。在现实生活中,总是要考虑不变的因素。
  • 不应该少一些bucket吗?说A.size() / some_const?还是一个固定的数字(10、100)?
  • 致 Peter G.:这几乎是真的,但在这个场景中我不这么认为。我认为尺寸足够大,最重要的是时间的增加不是 O(n) 而是大约 O (n^1.26)。
  • 在这种情况下,我不会自己编写任何排序,而是使用迄今为止最快的解决方案:STL 的排序
  • +1 只是为了漂亮的图表和视觉背景:希望每个问题都遇到这么多麻烦

标签: c++ algorithm performance stl


【解决方案1】:

在我看来,这里最大的瓶颈是内存管理功能(比如newdelete)。

Quicksort(其中 STL 可能使用了优化版本)可以就地对数组进行排序,这意味着它绝对不需要堆分配。这就是它在实践中表现如此出色的原因。

桶排序依赖于额外的工作空间,这在理论上是很容易获得的(即假设内存分配根本不需要时间)。在实践中,内存分配可能需要从(大)恒定时间到所请求内存大小的线性时间(例如,Windows 在分配页面时将花费时间将页面内容归零)。这意味着标准的链表实现将受到影响,并主导排序的运行时间。

尝试使用为大量项目预分配内存的自定义列表实现,您应该会看到排序运行得更快。

【讨论】:

  • 我试过用vector作为bucket(使用push_back pop_back,并为两个double预留空间),代码运行速度比使用list快,但放入bucket也消耗大部分时间。问题是一些bucket将有更大的内容。如果为每个桶预先分配它会浪费大量的内存和时间。现在我不知道最大桶的大小分布。
  • 这些正是桶排序运行良好所需的条件:它假设您有足够的额外空间随时可用。
  • 这也是为什么桶排序对于大型数据集不切实际的原因。
【解决方案2】:

iarray<List> buckets(numBuckets);

您基本上是在创建大量列表,这可能会花费您很多,尤其是在内存访问方面,理论上它是线性的,但实际上并非如此。

尽量减少桶数。

要验证我的断言,仅通过创建列表来分析您的代码速度。

还要遍历列表的元素,你不应该使用.size(),而是

//get back from buckets
for(size_t i=0,head=0;i!=numBuckets;i++)
  while(!buckets[i].empty())
  {
    A[head++] = buckets[i].front();
    buckets[i].pop_front();
  }

在某些实现中,.size() 可以在 O(n) 中。不太可能,但是...


经过一番研究,我发现 this page 解释 std::_List_node_base::hook 的代码是什么。

似乎只是在列表中的给定位置插入一个元素。应该不会花很多钱..

【讨论】:

  • size() 在我的环境(GCC)中似乎是不变的,我会尝试你的第一个想法
  • 即使不变,从列表中获取所有元素的“正确”方法是使用 empty/front/pop。
  • 它无法解释那么多时间。我只用初始化桶(只保留前两行)运行函数,从 n=1000 到 4096000 ,运行时间是原始的 2%-5%。
  • 好的。试试那个+“放在桶里”而不是“回来”。查看问题出在哪一行。
  • 我将尝试使用数组来存储桶的大小,并根据大小信息直接将数组复制到一个新的。因此避免使用链表。我将检查运行时间。
【解决方案3】:

链表不是数组。它们执行查找等操作的速度要慢得多。 STL 排序很可能有一个特定版本的列表,它会考虑到这一点并对其进行优化——但你的函数会盲目地忽略它正在使用的容器。您应该尝试使用 STL 向量作为数组。

【讨论】:

  • STL列表仅用作bucket,仅使用push_back front和pop_front,这应该需要const时间。唯一的排序是insertionsort(A),其中A实际上是double类型的数组。
  • @luoq:你在列表中调用size(),这是 O(N) 而不是 O(1)。
  • @Oli Charlesworth :在良好的实现中应该是 O(1)。此外,他的存储桶与数组中元素的数量一样多。如果分布均匀,则每个列表的长度应为 ~1。
  • @Loic: O(1) size() 与 O(1) splice() 冲突,所以也许,也许不是。 GNU 实现是一个 O(N) 时间的例子(它调用std::distance()
  • @Loïc Février:查看 SGI 文档,它们记录了函数的复杂性。如果您查看“新成员”部分,splice 的所有版本,包括“范围”版本都记录为“此函数是常数时间。”。要成为常数时间,不得在该范围内进行迭代!见sgi.com/tech/stl/List.html
【解决方案4】:

我认为有趣的问题可能是,你为什么要创建大量的存储桶?

考虑输入{1,2,3}, numBuckets = 3。包含buckets[int(numBuckets*A[i])].push_back(A[i]); 的循环将展开到

buckets[3].push_back(1);  
buckets[6].push_back(2);  
buckets[9].push_back(3);  

真的吗?三个值的九个桶......

考虑是否传递了范围 1..100 的排列。您将创建 10,000 个存储桶,并且只使用其中的 1%。 ...并且每个未使用的存储桶都需要在其中创建一个列表。 ...并且必须进行迭代,然后在读出循环中丢弃。

更令人兴奋的是,对列表进行排序 1..70000 并观看您的堆管理器爆炸式地尝试创建 49 亿个列表。

【讨论】:

  • 数组的内容是在[0,1)中随机生成的,所以只创建了numBuckets个桶,所有元素都可以放入。
【解决方案5】:

我并没有真正了解您的代码细节,因为在我学习的这一点上我对 Java 的了解还不够,虽然我在算法和 C 编程方面有一些经验,所以这是我的看法:

桶排序假设数组上的元素公平分布,这实际上更像是桶排序在 O(n) 上工作的条件,请注意在最坏的情况下,可能是您将大量元素放在 1您的存储桶,因此在下一次迭代中,您将处理与您最初尝试修复的几乎相同的问题,这会导致您的性能不佳。

请注意,桶排序的实际时间复杂度是 O(n+k),其中 k 是桶的数量,你数过你的桶了吗? k=O(n)?

桶排序中最浪费时间的问题是在分区到桶之后的空桶,当连接你的排序桶时,如果不实际测试它,你无法判断桶是否为空。

希望我能帮上忙。

【讨论】:

    猜你喜欢
    • 2020-08-26
    • 1970-01-01
    • 2010-11-03
    • 2011-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-07
    • 2023-03-29
    相关资源
    最近更新 更多