为什么 std::priority_queue 对其容器的元素进行排序?

问题描述

我注意到 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 以构建堆。那么元素是如何神奇地排序的呢?即使有一些事情,它是如何在线性时间内发生的?

解决方法

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

这不是真的。

std::priority_queue 可以按顺序提供元素,但它们没有排序。弹出的操作不仅仅是“取顶部元素”。

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

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

诀窍在于优先级队列使用一种称为“堆”的数据结构,或者说其复杂性等同于“堆”。这不要与 C/C++ 程序员赋予 mallocnew 分配的内存“自由存储”的俗称混淆。

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

一个非常非常天真的想法是想象一个二叉树,其中每个孩子都保证小于父母。

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

这不是完全堆的工作方式,但它为您提供了一种直观的方式来理解为什么它是可行的。

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

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

,

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

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

换句话说std::priority_queue 是一个经过优化的容器,可提供对逻辑上属于“顶部”的对象的快速访问,并假设其他元素不会访问,直到它们属于顶部。

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

即使有什么东西,它是如何在线性时间内发生的?

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

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