我有一个指向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;
}
答案 0 :(得分:8)
基于http://www.sgi.com/tech/stl/priority_queue.html,看起来没有办法做到这一点,没有清空和重新插入。
如果你愿意离开priority_queue(但仍然想要一个堆),那么你可以使用一个向量,以及make_heap,push_heap和pop_heap。请参阅priority_queue页面中的“备注”部分。
答案 1 :(得分:8)
这有点骇人听闻,但没有任何违法行为,它完成了工作。
std::make_heap(const_cast<city**>(&cities.top()),
const_cast<city**>(&cities.top()) + cities.size(),
Compare());
<强>更新强>:
如果出现以下情况,请不要使用此黑客:
vector
。Compare
仿函数的行为会导致外部副本的订购方式与Compare
中存储的priority_queue
副本不同。您始终可以编写自己的容器适配器来包装堆算法。 priority_queue
只不过是make/push/pop_heap
的简单包装。
答案 2 :(得分:7)
如果您需要保留有序集合,可以考虑以下解决方案:使用std::set
并更新值删除项目,更新其值并将其放回集合中。这将为您提供更新一个项目的O(log n)复杂度,这是在通常的堆结构中最好的(假设您可以通过筛选筛选程序访问其内部组件)。
std::set
的唯一缺点是初始化具有n个项目的集合的时间。 (O(n log n)代替O(n))。
答案 3 :(得分:3)
这是一个老问题,但当我想自己做这个时,我对任何答案都不满意。没有任何黑客攻击。 std::priority_queue
包含合法和惯用的所有机制。
std::priority_queue
有两个非常有用的数据成员c
(基础容器)和comp
(比较谓词)。
同样有用的是,标准要求Container
模板类型必须是SequenceContainer
的模型,其迭代器是RandomAccessIterator
的模型。
这很有用,因为Iter
std::make_heap
参数类型具有相同的RandomAccessIterator
模型要求。
这是一种冗长的说法,std::priority_queue
是堆的包装,因此std::make_heap(std::begin(c), std::end(c), comp)
必须是有效的表达式。
“坏”新闻是c
和comp
受到保护。这实际上是个好消息有两个原因:
您无法意外破坏堆。
如果您从std::priority_queue
派生,则可以有意修改堆。
所以诀窍是从std::priority_queue
派生你的优先级队列,在一个成员函数中,以任何你喜欢的方式改变内部堆c
,然后调用std::make_heap(std::begin(c), std::end(c), comp);
将它转回一个有效的堆。
从STL容器继承是不是一般的坏主意
嗯,是的,但是......
对于年轻人和/或不谨慎的人来说,这可能是一个坏主意有两个原因。缺乏多态析构函数和切片风险。
通过指向其基类的指针,拥有 std容器实际上没有合理的用例。容器是轻量级的(当它们没有任何东西时)并且便宜地可移动。您可以考虑用例,但我可以保证,通过将容器封装在另一个堆分配的对象中,可以更好地完成您打算做的任何事情。在精心设计的代码中,这应该永远不会成为一个问题。无论如何,从priority_queue
模板类私下继承可以消除这种风险。
当我们传递继承的对象时,当然存在切片的风险。这里的答案是从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));
}
答案 4 :(得分:1)
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