问题是使用std::list
实现O(1)删除列表项的推荐方法是什么?
通常,当我选择双向链表时,我希望能够在O(1)时间内从列表中删除元素,然后在O(1)时间内将其移动到不同的列表中。如果元素有自己的prev
和next
指针,那么完成工作就没有真正的诀窍。如果列表是双向链接循环列表,则删除不一定需要知道包含该项目的列表。
根据Iterator invalidation rules,std::list
迭代器非常耐用。所以,在我自己的项目中使用std::list
时,我似乎得到了我想要的行为,就是在我的类中隐藏一个迭代器,以及包含列表。
class Item {
typedef std::shared_ptr<Item> Ptr;
struct Ref {
std::list<Ptr>::iterator iter_;
std::list<Ptr> *list_;
};
Ref ref_;
//...
};
这有一个缺点,我需要创建我自己的std::list
装饰版本,知道每当项目添加到列表时更新ref_
。我想不出一种不需要嵌入式迭代器的方法,因为没有一种方法意味着擦除会首先产生O(n)查找操作。
使用std::list
获取O(1)删除的推荐方法是什么?或者,有没有更好的方法来实现目标?
过去,我通过实现自己的列表数据结构来完成此任务,其中放置在列表中的项目具有自己的next和prev指针。管理这些指针很自然,因为它们是列表操作本身固有的(我的列表实现的API调整指针)。如果我想使用STL,那么最好的方法是什么?我提出了嵌入迭代器的稻草人提议。有更好的方法吗?
如果需要具体的用例,请考虑使用计时器。创建计时器时,会将其放入适当的列表中。如果取消,则希望有效地将其除去。 (这个特定的例子可以通过标记而不是删除来解决,但它是实现取消的有效方法。)可根据要求提供其他用例。
我探索的另一个选择是将std::list
与std::unordered_map
融合以创建指针类型的专用列表。这是更重量级的(因为哈希表),但是提供了一个非常接近接口级标准容器的容器,并且给了我O(1)列表元素的擦除。稻草人提案中缺少的唯一特征是指向当前包含该项目的列表的指针。我已在CodeReview提出当前的实施,以征求意见。
答案 0 :(得分:2)
std::list::erase
保证为O(1)。
没有太多其他方法可以从标准列表中删除元素。 (std::list::remove
和朋友不做同样的事情,所以他们不算数。)
如果要从标准列表中删除,则需要一个迭代器和列表本身。这就是你似乎已经拥有的东西。以不同方式做这件事的自由度不大。我会将列表包含与对象分开,这与你所做的不同,因为为什么要创建一个一次只能在一个列表中的对象?对我来说似乎是一种不必要的人为限制。但无论你的设计有什么用途。
答案 1 :(得分:2)
也许您可以重新设计界面来分发迭代器而不是原始对象?对于你的计时器例子:
class Timer {
// ...
};
typedef std::list<Timer>::iterator TimerRef;
class Timers {
public:
TimerRef createTimer(long time);
void cancelTimer(TimerRef ref);
private:
std::list<Timer> timers;
};
当然,而不是
timer.cancel();
班上的用户现在不得不说
timers.cancelTimer(timerRef);
但根据您的使用情况,这可能不是问题。
更新:在列表之间移动计时器:
class Timers {
public:
Timer removeTimer(TimerRef ref);
void addTimer(Timer const &timer);
// ...
};
用法:
timers2.addTimer(timers1.removeTimer(timerRef));
不可否认,它有点麻烦,但替代方案也是如此。
答案 2 :(得分:1)
无法从std :: list中删除O(1)。
您可能需要考虑使用intrusive list
,其中列表节点直接嵌入到结构中,就像您已经完成的那样。
您可以使用boost::intrusive或自己动手,也可以查看this
答案 3 :(得分:1)
这是使用嵌入式iterator
的“完整”解决方案。一些私人特征用于帮助减少课堂上的混乱:
template <typename T> class List;
template <typename T>
class ListTraits {
protected:
typedef std::list<std::shared_ptr<T>> Impl;
typedef typename Impl::iterator Iterator;
typedef typename Impl::const_iterator ConstIterator;
typedef typename Impl::reverse_iterator Rotareti;
typedef typename Impl::const_reverse_iterator ConstRotareti;
typedef std::map<const List<T> *, typename Impl::iterator> Ref;
};
如图所示,列表实现将使用std::list
,但基础值类型将为std::shared_ptr
。我所追求的是允许T
的实例有效地派生自己的iterator
,以实现O(1)擦除。这是通过使用Ref
在项目插入列表后记住项目的迭代器来完成的。
template <typename T>
class List : public ListTraits<T> {
template <typename ITER> class IteratorT;
typedef ListTraits<T> Traits;
typename Traits::Impl impl_;
public:
typedef typename Traits::Impl::size_type size_type;
typedef typename Traits::Impl::value_type pointer;
typedef pointer value_type;
typedef IteratorT<typename Traits::Iterator> iterator;
typedef IteratorT<typename Traits::ConstIterator> const_iterator;
typedef IteratorT<typename Traits::Rotareti> reverse_iterator;
typedef IteratorT<typename Traits::ConstRotareti> const_reverse_iterator;
class Item;
~List () { while (!empty()) pop_front(); }
size_type size () const { return impl_.size(); }
bool empty () const { return impl_.empty(); }
iterator begin () { return impl_.begin(); }
iterator end () { return impl_.end(); }
const_iterator begin () const { return impl_.begin(); }
const_iterator end () const { return impl_.end(); }
reverse_iterator rbegin () { return impl_.rbegin(); }
reverse_iterator rend () { return impl_.rend(); }
const_reverse_iterator rbegin () const { return impl_.rbegin(); }
const_reverse_iterator rend () const { return impl_.rend(); }
pointer front () const { return !empty() ? impl_.front() : pointer(); }
pointer back () const { return !empty() ? impl_.back() : pointer(); }
void push_front (const pointer &e);
void pop_front ();
void push_back (const pointer &e);
void pop_back ();
void erase (const pointer &e);
bool contains (const pointer &e) const;
};
这个List
主要是一个像接口这样的队列。但是,可以从列表中的任何位置删除项目。简单函数主要是委托给基础std::list
。但push_*()
和pop_*()
方法也会记住iterator
。
template <typename T>
template <typename ITER>
class List<T>::IteratorT : public ITER {
friend class List<T>;
ITER iter_;
IteratorT (ITER i) : iter_(i) {}
public:
IteratorT () : iter_() {}
IteratorT & operator ++ () { ++iter_; return *this; }
IteratorT & operator -- () { --iter_; return *this; }
IteratorT operator ++ (int) { return iter_++; }
IteratorT operator -- (int) { return iter_--; }
bool operator == (const IteratorT &x) const { return iter_ == x.iter_; }
bool operator != (const IteratorT &x) const { return iter_ != x.iter_; }
T & operator * () const { return **iter_; }
pointer operator -> () const { return *iter_; }
};
这是用于定义List
的迭代器类型的帮助器模板的实现。它的不同之处在于,*
和->
运算符的定义方式使迭代器的行为类似于T *
而不是std::shared_ptr<T> *
(这就是底层迭代器通常会这样做。
template <typename T>
class List<T>::Item {
friend class List<T>;
mutable typename Traits::Ref ref_;
};
从T
派生的List<T>::Item
类型可以添加到List<T>
。此基类包含用于在将项添加到列表时记住迭代器的Ref
实例。
template <typename T>
inline void List<T>::push_front (const pointer &e) {
const Item &item = *e;
typename Traits::Ref::iterator i = item.ref_.find(this);
if (i == item.ref_.end()) {
item.ref_[this] = impl_.insert(impl_.begin(), e);
} else if (front() != e) {
impl_.erase(i->second);
i->second = impl_.insert(impl_.begin(), e);
}
}
template <typename T>
inline void List<T>::pop_front () {
if (!empty()) {
const Item &item = *front();
item.ref_.erase(this);
impl_.pop_front();
}
}
此代码说明了如何执行memoization。执行push_front()
时,首先检查项目是否已包含该项目。如果不是,则插入它,并将生成的迭代器添加到ref_
对象中。否则,如果它不是前面的,则删除该项并在前面重新插入,并更新memoized迭代器。 pop_front()
删除已记住的迭代器,然后在pop_front()
上调用std::list
。
template <typename T>
inline void List<T>::push_back (const pointer &e) {
const Item &item = *e;
typename Traits::Ref::iterator i = item.ref_.find(this);
if (i == item.ref_.end()) {
item.ref_[this] = impl_.insert(impl_.end(), e);
} else if (back() != e) {
impl_.erase(i->second);
i->second = impl_.insert(impl_.end(), e);
}
}
template <typename T>
inline void List<T>::pop_back () {
if (!empty()) {
const Item &item = *back();
item.ref_.erase(this);
impl_.pop_back();
}
}
push_back()
和pop_back()
与push_front()
和pop_front()
类似。
template <typename T>
inline void List<T>::erase (const pointer &e) {
const Item &item = *e;
typename Traits::Ref::iterator i = item.ref_.find(this);
if (i != item.ref_.end()) {
item.ref_.erase(i);
impl_.erase(i->second);
}
}
erase()
例程检索memoized迭代器,并使用它来执行擦除。
template <typename T>
inline bool List<T>::contains (const pointer &e) const {
const Item &item = *e;
typename Traits::Ref::iterator i = item.ref_.find(this);
return i != item.ref_.end();
}
由于项目在很多方面都是自己的迭代器,因此在find()
版本中不应该使用List
方法。但是,代替这个是contains()
方法,用于查看元素是否是列表的成员。
现在,所提出的解决方案使用std::map
将列表实例与迭代器相关联。如果项目同时是一个成员的列表数量相对较小,则保持O(1)的精神。
接下来我会试着boost::intrusive
版本。
答案 4 :(得分:0)
可怕的事实:虽然链表是强大的结构,但std::list
无法充分利用它的功能。
你不能仅使用迭代器从std::list
擦除对象,因为list必须释放节点,你必须知道分配内存的分配器在哪里(提示:它在列表中)。
与标准链表相比,侵入式容器具有许多优点,例如自我意识;-),按值存储多态对象的能力以及它们使列表技巧(如在多个列表中具有单个对象)可行。由于您无论如何都不直接使用std::list
,因此您可以完全停止使用std::list
并使用第三方容器,或自行推送。
(此外,您的解决方案也具有侵入性,因为您的类型必须来自List<T>::Item
,这会对std::list
没有的类型提出某些要求
答案 5 :(得分:-1)
无法完成。列表使用指向“相邻”列表项的前向和/或后向指针。因此,每次做某事时都需要迭代。
如果您希望O(1)用于某种集合,请使用数组来处理您的内容。
但是,对于记录来说,扫描一个少于100个元素的列表会花费一段不明显的时间,除非你每秒钟进行一千次。
编辑:
如果您知道要删除的NODE,那就很容易了。我不确定STD列表究竟是如何工作的,但从理论上讲,节点应该能够通过将其1或2个相邻节点的前向和后向指针设置为相互指向来自行删除。