【问题标题】:How to tell a std::priority_queue to refresh its ordering?如何告诉 std::priority_queue 刷新其排序?
【发布时间】:2011-04-27 20:31:27
【问题描述】:

我有一个指向struct city 的指针的优先级队列。我在优先级队列之外修改了这些指针指向的对象,并想告诉优先级队列根据新值“重新排序”自己。

我该怎么办?

例子:

#include <iostream>
#include <queue>

using namespace std;

struct city {
    int data;
    city *previous;
};

struct Compare {
    bool operator() ( city *lhs, city *rhs )
    {
        return ( ( lhs -> data ) >= ( rhs -> data ) );
    }
};

typedef priority_queue< city *, vector< city * >, Compare > pqueue;

int main()
{
    pqueue cities;

    city *city1 = new city;
    city1 -> data = 5;
    city1 -> previous = NULL;
    cities.push( city1 );

    city *city2 = new city;
    city2 -> data = 3;
    city2 -> previous = NULL;
    cities.push( city2 );

    city1 -> data = 2;
    // Now how do I tell my priority_queue to reorder itself so that city1 is at the top now?

    cout << ( cities.top() -> data ) << "\n";
    // 3 is printed :(

    return 0;
}

【问题讨论】:

  • 这正是我遇到的问题!根据 cppreference.com 中的文档,一旦我们调用 push 函数,它应该重新排序队列。我花了几个月的时间才找到这个错误……我最终使用了 make_heap 和 push_heap 函数来工作。但我仍然想切换回priority_queue,因为这是一个更清洁的解决方案!

标签: c++ stl priority-queue


【解决方案1】:

这有点骇人听闻,但没有任何违法之处,它可以完成工作。

std::make_heap(const_cast<city**>(&cities.top()),
               const_cast<city**>(&cities.top()) + cities.size(),
               Compare());

更新

在以下情况下请勿使用此 hack:

  • 底层容器不是vector
  • Compare 函子的行为会导致您的外部副本与存储在priority_queue 中的Compare 副本的顺序不同。
  • 您并不完全理解这些警告的含义。

您始终可以编写自己的容器适配器来包装堆算法。 priority_queue 只不过是 make/push/pop_heap 的简单包装。

【讨论】:

  • std::priority_queue 不保证它的元素是连续的,除非底层容器提供这种保证。
  • 同意。这是“hackishness”的一部分。这个例子中的底层容器是向量,它保证了一个连续的缓冲区。 “hackishness”的另一部分是比较器应该是无状态的。然后是 const_cast ,它会吓跑许多代码审查员,但实际上在这个例子中做了完全正确的事情。这个例子有效。但是不要将容器更改为 deque,不要提供有状态的比较器,也不要通过“过于频繁地”调用 make_heap 来滥用它,以免性能下降。明智地使用,这个技巧很有效。
  • 这对我有用,谢谢:)。为了将来参考,有状态比较器是什么意思?对不起,如果这个问题很愚蠢。
  • 我不确定priority_queue 是否能保证与make_heap 和朋友互操作。哈克,确实。
  • 这很安静,但请告诉我运行时间是 O(log(n))...它似乎是线性的,而不是真正的对数。
【解决方案2】:

如果您需要保留有序集合,您可以考虑以下解决方案:使用std::set 并更新值删除项目,更新其值并将其放回集合中。这会给你更新一个项目的 O(log n) 复杂性,这是你在通常的堆结构中所能做到的最好的(假设你可以通过 sift-up sift-down 过程访问它的内部来进行质量)。 std::set 的唯一缺点是初始化包含 n 个项目的集合的时间。 (O(n log n) 而不是 O(n))。

【讨论】:

  • 但是在一个集合中我不能有两个具有相同数据的元素,可以吗? :(
  • +1 对于这个解决方案,这比 Moron 的要高效得多。 @Skkard:在std::multiset,你可以。
  • 顺便说一句,为此 +1 :-) 我认为任何平衡二叉树也可以(这就是我猜 std:set 的实现方式)。 std::set/multiset 是否保证 O(log n) 插入/删除和访问 max/min?
  • @lars:我误读了这个答案,我删除了我之前的评论。你是对的,这可能会更好。
  • 此解决方案每次修改都需要分配 + 解除分配。如果没有 2009 年 9 月 19 日对 LWG 839 的建议:open-std.org/jtc1/sc22/wg21/docs/lwg-closed.html#839,根据应用程序的不同,这样做的成本可能会过高。
【解决方案3】:

基于http://www.sgi.com/tech/stl/priority_queue.html,看起来没有办法做到这一点,无需清空和重新插入。

如果您愿意离开priority_queue(但仍然想要一个堆),那么您可以使用向量以及make_heappush_heappop_heap。请参阅页面中的注释部分以获取priority_queue

【讨论】:

  • 我正准备推荐make_heap 和朋友,但无法计算出算法。它是如何工作的?
  • @lars:make_heap/push_heap/pop_heap 页面有关于如何使用它们的示例和说明。 (或者你对内部结构感兴趣?)。
  • @Moron:我了解这些功能的工作原理。您是说 OP 应该在每次更新后致电 make_heap 吗?
  • @lars:如果比较函数发生变化(或指向的数据发生变化),那么是的。 make_heap 必须被调用。如果更新频繁,可能效率低下。如果只有一个change_key(我找不到)......不确定make_heap如何处理已经是“几乎堆”的数据。或许终究不会太低效……
【解决方案4】:

这是一个老问题,但当我想自己做这件事时,我对任何答案都不完全满意。不需要任何黑客攻击。 std::priority_queue 包含合法和惯用地执行此操作的所有机制。

std::priority_queue 有两个非常有用的数据成员,c(底层容器)和comp(比较谓词)。

同样有用的是,标准要求 Container 模板类型必须是 SequenceContainer 的模型,而迭代器是 RandomAccessIterator 的模型。

这很有帮助,因为 std::make_heapIter 参数类型具有相同的 RandomAccessIterator 模型要求。

这是一种冗长的说法,即std::priority_queue 是堆的包装器,因此std::make_heap(std::begin(c), std::end(c), comp) 必须是一个有效的表达式。

“坏”消息是 ccomp 受到保护。这实际上是个好消息,原因有两个:

  1. 您不能意外破坏堆。

  2. 如果您从std::priority_queue 派生,您可以有意修改堆。

因此,诀窍是从std::priority_queue 派生您的优先级队列,在成员函数中,以任何您喜欢的方式改变内部堆c,然后调用std::make_heap(std::begin(c), std::end(c), comp); 将其变回有效堆。

一般来说,从 STL 容器继承不是一个坏主意

嗯,是的,但是……

对于年轻人和/或粗心的人来说,这可能是一个坏主意,有两个原因。缺乏多态析构函数和切片的风险。

  1. 多态析构函数

实际上没有合理的用例拥有一个标准容器,通过一个指向它的基类的指针。容器是轻量级的(当其中没有任何东西时)并且可以廉价移动。您可能会想到用例,但我可以保证,通过将容器封装在另一个堆分配的对象中,可以将您打算做的任何事情做得更好。在精心设计的代码中,这不应该成为一个问题。无论如何,从priority_queue 模板类中私有继承可以消除这种风险。

  1. 切片

当我们传递继承的对象时,当然存在切片的风险。这里的答案是从priority_queue基类私有继承,然后在派生类中使用using只导出我们希望共享的基类接口部分。

下面的示例已更新以显示这一点。

下面是一个真实项目的例子。

要求: 保留必须通知客户更改的主题队列。按通知此主题的最早时间的时间戳对队列进行排序。不允许重复的主题名称。

这是一个工作演示:

#include <queue>
#include <string>
#include <chrono>
#include <cassert>
#include <iostream>

using topic_type = std::string;
using timestamp_clock = std::chrono::system_clock;
using timestamp_type = timestamp_clock::time_point;

struct notification {
    topic_type topic;
    timestamp_type timestamp;
};

bool topic_equals(const notification& l, const topic_type& r) {
    return l.topic == r;
}
bool topic_equals(const topic_type& l, const notification& r) {
    return l == r.topic;
}

bool age_after(const notification& l , const notification& r) {
    return l.timestamp > r.timestamp;
}

bool age_after(const notification& l , const timestamp_type& r) {
    return l.timestamp > r;
}

bool age_after(const timestamp_type& l , const notification& r) {
    return l > r.timestamp;
}

struct greater_age
{
    template<class T, class U>
    bool operator()(const T& l, const U& r) const {
        return age_after(l, r);
    }
};

template<class T>
struct pending_queue_traits;

template<>
struct pending_queue_traits<notification>
{
    using container_type = std::vector<notification>;
    using predicate_type = greater_age;
    using type = std::priority_queue<notification, container_type, predicate_type>;
};

class pending_notification_queue
: private pending_queue_traits<notification>::type
{
    using traits_type = pending_queue_traits<notification>;
    using base_class = traits_type::type;

public:


    // export the constructor
    using base_class::base_class;

    // and any other members our clients will need
    using base_class::top;
    using base_class::pop;
    using base_class::size;

    bool conditional_add(topic_type topic, timestamp_type timestamp = timestamp_clock::now())
    {
        auto same_topic = [&topic](auto& x) { return topic_equals(topic, x); };
        auto i = std::find_if(std::begin(c), std::end(c), same_topic);
        if (i == std::end(c)) {
            this->push(notification{std::move(topic), std::move(timestamp)});
            return true;
        }
        else {
            if (timestamp < i->timestamp) {
                i->timestamp = std::move(timestamp);
                reorder();
                return true;
            }
        }
        return false;
    }

private:
    void reorder() {
        std::make_heap(std::begin(c), std::end(c), comp);
    }
};

// attempt to steal only the base class...
void try_to_slice(pending_queue_traits<notification>::type naughty_slice)
{
    // danger lurks here
}
int main()
{
    using namespace std::literals;

    auto pn = pending_notification_queue();

    auto now = timestamp_clock::now();
    pn.conditional_add("bar.baz", now);
    pn.conditional_add("foo.bar", now + 5ms);
    pn.conditional_add("foo.bar", now + 10ms);
    pn.conditional_add("foo.bar", now - 10ms);

    // assert that there are only 2 notifications
    assert(pn.size() == 2);
    assert(pn.top().topic == "foo.bar");
    pn.pop();
    assert(pn.top().topic == "bar.baz");
    pn.pop();

    // try to slice the container. these expressions won't compile.
//    try_to_slice(pn);
//    try_to_slice(std::move(pn));

}

【讨论】:

  • 由于破坏问题而从 STL 容器(甚至容器适配器,如 std::priority_queue)继承不是一个坏主意吗? (即 STL 容器具有非虚拟析构函数)
  • 总的来说,我会同意。人们应该警惕从容器继承,而依赖它们的多态破坏肯定是错误的。但是,那些受保护的成员存在于priority_queue 中,专门用于此用例。我们实际上可以更进一步,并争辩说没有一个有效的用例来直接在堆上分配一个带有 new 的容器,在这种情况下,永远不需要多态销毁。
  • @ArchbishopOfBanterbury 很棒的评论 - 我已经在更新的答案中解决了这个问题。有趣的是,在 c++ 社区中,doing this has some caveats 的想法经常变成never, ever do this, ever!。高级开发人员在向初级开发人员解释事情时有时会很懒惰:)
  • 我明白了,这很有道理,+1
【解决方案5】:

stl 的容器没有提供尽可能快的可更新优先级队列。

@Richard Hodges:make_heap 需要 O(n) 复杂度,push_heap 函数不会告诉您提供的元素的存储位置,因此无法快速更新单个元素(您需要 O(n ) 来找到它)。

我已经实现了一个高性能的可更新优先级队列(更新成本 O(log n),是使用 std::set 的两倍)并使其可用 on github

这就是你通常使用它的方式:

better_priority_queue::updatable_priority_queue<int,int> pQ;
pQ.push(0, 30);   // or pQ.set(0, 30)
pQ.push(1, 20);
pQ.push(2, 10);

pQ.update(2, 25); // or pQ.set(2, 25)

while(!pQ.empty())
    std::cout << pQ.pop_value().key << ' ';

// Outputs: 0 2 1

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-12-09
    • 1970-01-01
    • 2023-02-09
    • 2012-01-05
    • 1970-01-01
    • 2014-06-14
    • 2021-09-09
    • 2019-11-16
    相关资源
    最近更新 更多