【问题标题】:Remove an element from the middle of an std::heap从 std::heap 中间移除一个元素
【发布时间】:2011-06-11 22:12:55
【问题描述】:

我正在使用优先级队列作为调度程序,但有一个额外的要求。我需要能够取消预定的项目。这相当于从优先级队列的中间移除一个项目。

我不能使用std::priority_queue,因为对除 top 之外的任何元素的访问都是受保护的。

我正在尝试使用algorithm 的堆函数。但我仍然缺少我需要的那块。当我从堆中间删除一个元素时,我希望它能够有效地重建自己。 C++ 提供了这些堆函数:

  • std::make_heap O(3n)
  • std::push_heap O(lg(n))
  • std::pop_heap O(2 lg(n))

我想要一个像 std::repair_heap 这样的新函数,带有一个大 O 3n。我会向它提供被取消项目所在的洞的位置,它会正确调整堆。

不提供std::repair_heap 函数似乎是一个巨大的疏忽。我错过了什么明显的东西吗?

是否有提供符合 stl 的 std::repair_heap 的库?

是否有更好的数据结构来建模调度程序?

注意
我没有使用std::map 有几个原因。

  • 堆具有恒定的内存开销。
  • 堆具有出色的缓存局部性。

【问题讨论】:

  • 如果我错了请纠正我,但我认为您必须为此致电std::make_heap,因为无论如何您都必须移动元素。
  • 我可以使用std::make_heap。但感觉应该有一个更快的选择。我怀疑repair_heap 可以写成O(lg(n)),比如push 和pop。我的推理是 repair_heap 只是从堆的中间而不是头部弹出。
  • 这确实很挑剔,但是虽然说 O(3n) 或 O(2 lg n) 在技术上是正确的,但通常不会这样做,因为它忽略了大 O。 Big-O 在不考虑常数的情况下对相对增长率进行分类。不用写 O(3n),只写 O(n)。同样,不要写 O(2 lg n),写 O(lg n)。现在,如果您确实想说您的代码最多运行 2 次 lg n 比较,那很好 - 直接说出来,不要使用 big-O 表示法。
  • @templatetypedef,在这种情况下,OP实际上只是采用cplusplus.com's的方式来解释他们的算法的时间复杂度,所以我们不应该为此责备OP。
  • @Richard 查看您链接的页面,它说“在第一个和最后一个之间的距离的三倍内达到线性。”这与说 O(3n); 不一样;其中第一个是一个非常具体的界限,没有渐近指定,而第二个在技术上是正确的,但具有误导性。

标签: c++ data-structures stl priority-queue


【解决方案1】:

在我看来,从堆中间移除可能意味着必须重建整个堆:没有 repair_heap 的原因是因为它必须执行与 make_heap 相同的(大哦)工作。

您是否能够执行诸如将std::pair<bool, Item> 放入堆中并仅使项目无效而不是删除它们的操作?然后当他们最终到达顶部时,只需忽略该项目并继续前进。

【讨论】:

  • 忽略预定项目是我最初的策略。然而,调度程序的一位用户却因取消的项目而不知所措。少于 100 个主动安排项目,但有 1000 个取消项目。为了解决这个问题,我正在尝试添加真正的取消。
  • @deft_code,如何保留已取消项目的计数并仅在计数达到阈值时才重建堆?
  • 从堆中间删除一个元素永远不会比删除顶部元素更昂贵(O(lg n)),所以这应该是可能的,只是缺少来自 STL
  • @ChrisDodd 虽然从中间删除一个元素的行为应该是 O(log(n)),但问题是如果没有 第一次发现,你就无法删除某些东西 要删除的元素。由于堆的属性不能保证哪个子树将包含不是最高值的特定元素,因此找到任意项实际上是 O(n)。因此 find+ delete 也是 O(n)。 结论: Mark B 关于整体操作仍然是 O(n) 是正确的,但原因是错误的。 而且“修复堆”可能没那么有用;不同的方法/结构可能会更好。
【解决方案2】:

不幸的是,标准缺少这个(相当重要的)功能。使用g++,你可以使用非标准函数std::__adjust_heap来做到这一点,但是没有简单的可移植方式——而且__adjust_heap在不同版本的g++中略有不同,所以你甚至不能这样做可移植于 g++ 版本。

【讨论】:

  • std::__adjust_heap 看起来是正确的,但没有任何文档。特别是我不知道__value 参数是干什么用的。
  • stackoverflow.com/questions/228783/… 建议像__adjust_heap 这样以“__”开头的函数仅用于实现。
  • @gregg: __adjust_heap is 仅用于实施。这使得使用该功能有点冒险,因为该功能没有记录,可能会更改签名或完全消失。更糟糕的情况是__adjust_heap 更改语义是一种不会更改其签名的方式。例如,它停止调整堆,现在随机化它。所以我在使用它时必须非常小心,并编写一些很棒的单元测试。
  • 我不认为这个功能那么重要。在堆中间找到一个项目(以便可以删除)已经是 O(n)。因此,如果这是需要做很多事情的事情:“修复/调整堆”功能将远不如更合适的算法/数据结构有用。
【解决方案3】:

您的repair_heap() 是如何工作的?这是我的猜测:

如果您的堆是由某个迭代器范围定义的,例如 (heapBegin, heapEnd)。您要删除的元素是堆的某个子树的根,它由某个子范围(subHeapBegin,subHeapEnd)定义。使用std::pop_heap(subHeapBegin, subHeapEnd),然后如果subHeapEnd != heapEnd,交换*(subHeapEnd-1)*(heapEnd-1) 的值,这应该将您删除的项目放在堆容器的末尾。现在您必须在子堆中向上渗透 *(subHeapEnd-1) 处的元素。如果我没有遗漏任何东西,这是可能的,那么剩下的就是将结束元素从堆容器中切掉。

在尝试正确编码之前(我已经跳过了一些细节,比如计算 subHeapBegin 和 subHeapEnd),我会运行一些测试来确定 make_heap() 是否真的让你慢下来。 Big-O 很有用,但它与实际执行时间不同。

【讨论】:

    【解决方案4】:

    这是我用来从堆中删除项目的一些德尔福代码。我不知道你说的这个C++没有修复功能,但是嘿嘿..

    首先是流行音乐,这样您就可以了解它是如何工作的:

    function THeap.Pop: HeapItem;
    begin
      if fNextIndex > 1 then begin
        Dec(fNextIndex);
        Result:= fBuckets[1];   //no zero element
        fBuckets[1] := fBuckets[fNextIndex];
        fBuckets[fNextIndex] := nil;
        FixHeapDown;            //this has a param defaulting to 
        end
      else
        Result:= nil;
    end;
    

    现在对比一下,删除:

    procedure THeap.Delete(Item: HeapItem);
    var
      i:integer;
    begin
      for i:=1 to pred(fNextIndex) do
        if Item=fBuckets[i] then begin
          dec(fNextIndex);
          fBuckets[i] := fBuckets[fNextIndex];
          fBuckets[fNextIndex] := nil;
          FixHeapDown(i);
          break;
          end;
    end;
    

    这当然是不可以考虑的 做我们在这里做的事情,但是,嘿,成本 有时确实会发生变化,工作确实会被取消。

    享受。 我希望这会有所帮助。

    【讨论】:

      【解决方案5】:

      我猜你知道要删除堆容器中的哪个元素(索引 n)。

      1. 设置值v[n] = BIG;BIG 确实比堆中的任何其他值都大。
      2. 致电std::push_heap( v.begin(), v.begin()+n+1 );
      3. 致电std::pop_heap( v.begin(), v.end() );
      4. 致电v.pop_back();
      5. 完成

      运算为 O(ln(n))

      RE:要求证明

      首先,一个限定符: 这个方法假设了 std push_heap 使用的算法。
      具体来说,它假设 std push_heap( v.begin(), v.begin()+n+1 ) 只会改变范围 [0, n]
      对于那些是 n 的祖先的元素,即以下集合中的索引:

      A(n)={n,(n-1)/2,((n-1)/2-1)/2....0}.  
      

      这是 std push_heap 的典型规范:

      http://www.cplusplus.com/reference/algorithm/push_heap/ “给定一个堆范围 [first,last-1),此函数通过将 (last-1) 中的值放入其中的相应位置,将视为堆的范围扩展到 [first,last)。”

      它是否保证使用您在教科书中读到的“普通堆算法”? 你告诉我。

      无论如何,这是您可以运行并凭经验看到它有效的代码。 我正在使用 VC 2005。

      #include <algorithm>
      #include <vector>
      #include <iostream>
      
      bool is_heap_valid(const std::vector<int> &vin)
      {
          std::vector<int> v = vin;
          std::make_heap(v.begin(), v.end());
          return std::equal(vin.begin(), vin.end(), v.begin());
      }
      
      
      int _tmain(int argc, _TCHAR* argv[])
      {
          srand(0);
          std::vector<int> v;
          for (int i=0; i<100; i++)
          {
              v.push_back( rand() % 0x7fff );
          }
          std::make_heap(v.begin(), v.end());
      
          bool bfail = false;
          while( v.size() >= 2)
          {
              int n = v.size()/2;
              v[n] = 0x7fffffff;
              std::push_heap(v.begin(), v.begin()+n+1);
              std::pop_heap(v.begin(), v.end());
              v.resize(v.size()-1);
              if (!is_heap_valid(v))
              {
                  std::cout << "heap is not valid" << std::endl;
                  bfail = true;
                  break;
              }
          }
          if (!bfail)
              std::cout << "success" << std::endl;
      
          return 0;
      
      }
      

      但是我还有一个问题,就是如何知道需要删除的索引“n”。在使用 std push_heap 和 std pop_heap 时,我看不到如何跟踪它(知道堆中的位置)。我认为每次对象在堆中移动时,您都需要编写自己的堆代码并将堆中的索引写入对象。叹息。

      【讨论】:

      • 我不相信这会奏效。说服我,我会将接受的答案更改为这个答案。
      • 我认为如果你为你保留在堆中的类型重载 std::swap ,你实际上应该能够使用 std::push_heap 和 std::pop_heap 。在对象上创建一个成员,用于将索引存储到堆中,并在插入之前将其设置为堆的当前大小。这将是开始时正确的值,因为第一个堆插入步骤是使新元素成为最后一级的最后一个,因此它的索引是插入之前堆的大小。然后每次堆函数调用 std::swap 时,让您的覆盖交换存储在每个元素上的索引。
      • 不幸的是经验证据不足。事实上,即使是针对特定实现的完整数学证明也不好,因为另一个编译器或同一编译器的不同版本可能使用不同的算法。问题是:使堆成为“堆”的唯一因素是树结构维护的属性(它甚至不必是二进制) .堆实现将随机访问迭代器转换为虚拟树。因此,当您在 begin..begin()+n+1 上重新排列树时,堆属性在 begin..end 上仍然有效吗?
      【解决方案6】:

      你可以试试'std::multiset',它被实现为堆结构并支持'std::erase'操作,所以你可以'std::find'元素然后擦除它。

      【讨论】:

      • 这个答案不正确,因为 std::multiset 是自平衡二叉树,而不是二叉堆(树的中序遍历是排序的,而不是具有堆不变的树)
      猜你喜欢
      • 1970-01-01
      • 2014-11-11
      • 1970-01-01
      • 2015-06-21
      • 2017-05-16
      • 1970-01-01
      • 1970-01-01
      • 2011-11-13
      • 1970-01-01
      相关资源
      最近更新 更多