【问题标题】:Why is std::priority_queue sorting its container's elements?为什么 std::priority_queue 对其容器的元素进行排序?
【发布时间】:2021-09-09 04:11:56
【问题描述】:

我注意到std::priority_queue 以排序方式存储元素。显然,以排序方式存储元素将是一个糟糕的设计选择,因为 pushpop 的时间复杂度会飙升至 O(n)。但事实证明std::priority_queue 在线性时间内神奇地对元素进行排序。

这是我用于测试的代码。

#include <iostream>
#include <queue>
#include <algorithm>
#include <vector>
#include <chrono>
#include <random>
#include <climits>
#include <fstream>
#include <ios>

int main() {
  int size = 10'000'000;

  std::random_device rd;
  std::mt19937 mt{rd()};
  std::uniform_int_distribution<int> uid{1, INT32_MAX};

  std::vector<int> vs;
  for (int i = 0; i < size; ++i) {
    vs.push_back(uid(mt));
  }

  // Measures time taken by make_heap
  std::vector<int> vs1{vs};
  auto start = std::chrono::system_clock::now();
  std::make_heap(vs1.begin(), vs1.end());
  auto end = std::chrono::system_clock::now();
  std::chrono::duration<double> diff = end - start;
  std::cout << "Time taken by make_heap: " << diff.count() << std::endl;

  // Measures time taken by priority_queue
  std::vector<int> vs2{vs};
  start = std::chrono::system_clock::now();
  std::priority_queue<int, std::vector<int>, std::greater<int>> qs{vs2.begin(), vs2.end()};
  end = std::chrono::system_clock::now();
  diff = end - start;
  std::cout << "Time taken by priority_queue: " << diff.count() << std::endl;

  // Measures time taken by sort
  std::vector<int> vs3{vs};
  start = std::chrono::system_clock::now();
  std::sort(vs3.begin(), vs3.end());
  end = std::chrono::system_clock::now();
  diff = end - start;
  std::cout << "Time taken by sort: " << diff.count() << std::endl;
    
  std::ofstream ofile;
  ofile.open("priority_queue_op.txt", std::ios::out);
  for (int i = 0; i < size; ++i) {
    ofile << qs.top() << std::endl;
    qs.pop();
  }
  ofile.close();

  ofile.open("sort_op.txt", std::ios::out);
  for (auto& v : vs3)
    ofile << v << std::endl;
  ofile.close();

  // run `diff priority_queue_op.txt sort_op.txt`

  return 0;
}
$ g++ -O3 test.cpp -o test
$ ./test
Time taken by make_heap: 0.133292
Time taken by priority_queue: 0.151002
Time taken by sort: 0.910701
$ diff priority_queue_op.txt sort_op.txt
$

从上面的输出来看,std::priority_queue 似乎是在线性时间对元素进行排序。

This site 建议std::priority_queue 使用标准库中的堆函数在内部管理堆。甚至源代码也证实了这一点。

Line 596 - 605

      template<typename _InputIterator>
    priority_queue(_InputIterator __first, _InputIterator __last,
               const _Compare& __x = _Compare(),
               _Sequence&& __s = _Sequence())
    : c(std::move(__s)), comp(__x)
    {
      __glibcxx_requires_valid_range(__first, __last);
      c.insert(c.end(), __first, __last);
      std::make_heap(c.begin(), c.end(), comp);
    }

插入过程用于插入元素,后跟std::make_heap 以构建堆。那么元素是如何神奇地排序的呢?即使有什么事情是如何在线性时间内发生的?

【问题讨论】:

  • 为什么你认为它是排序的?每当您调用pop 时,容器都会调用std::pop_heap 以使下一个最多的元素进入“顶部”位置
  • 构建堆可以在线性时间内完成,尽管重复删除以按排序顺序查看它们不能。请参阅stackoverflow.com/questions/9755721/…,了解std::make_heap 如何在线性时间内执行。
  • 无关:使用system_clock 进行基准测试可能会因系统时钟的更改而混乱。 steady_clock 通常是更好的选择。
  • @NathanOliver 刚刚意识到它并没有以排序方式存储元素,而只是输出使它看起来好像元素已排序。这是因为我每次都打印顶部元素导致排序数组

标签: c++ sorting c++11 priority-queue


【解决方案1】:

那么元素是如何神奇地排序的呢?

它们没有“排序”。

它们的排列方式使得应该位于"top" 的元素位于正确的位置。其他元素保证会被排序。相反,它们被安排在所谓的heap 中。

换一种说法std::priority_queue 是一个经过优化的容器,可提供对逻辑上属于“顶部”的对象的快速访问,并假设其他元素在此之前不会被访问他们属于顶部。

“其他元素不会被访问”的条件允许组织比完全排序更快。

即使有什么事情是如何在线性时间内发生的?

如果您指的是插入/删除,它实际上是在对数时间中完成的。

在对数时间内,保证最前面的项在正确的位置,而其他所有的项都保证形成一个有效的堆。如果数据在插入/删除之前已经是堆,这不会花费很长时间。

【讨论】:

    【解决方案2】:

    我注意到std::priority_queue 以排序方式存储元素。

    这不是真的。

    std::priority_queue 可以按顺序提供元素,但不排序。弹出的操作确实比“取顶部元素”更有效。

    插入和提取需要对数时间。

    设置一批元素需要线性时间。

    诀窍在于优先级队列使用一种称为“堆”或复杂性等同于“堆”的数据结构。请不要将这与 C/C++ 程序员给 mallocnew 分配的内存“免费存储”的俗称混淆。

    堆的排序方式并非您想象的那样。它的排列方式是,当您从中抓取元素时,您知道最大(或最小)元素在哪里,并且您可以在对数时间内获取下一个最大元素。

    一种非常非常幼稚的想法是想象一棵二叉树,其中每个子树都保证小于父树。

    最大的元素是根。当您消除根时,您知道下一个最大元素是根的左子或右子;检查并推广它。

    这不是确切堆的工作方式,但它为您提供了一种直观的方式来理解为什么它是合理的。

    “堆”的神奇之处在于能够在随机访问缓冲区中执行此操作,而无需在对数时间内移动太多东西,并在同一个随机访问缓冲区中获取一组未排序的项目并将其排列成“堆”在线性时间内到位。

    Heap 的维基百科文章还不错。如果你想了解更多,那或者一本好的本科数据结构的教科书是不错的东西。

    【讨论】:

      猜你喜欢
      • 2011-12-09
      • 2018-09-18
      • 2021-01-21
      • 2021-06-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多