如何从std::列表中实现0(1)擦除

How to achieve O(1) erasure from a std::list

本文关键字:擦除 实现 std 列表      更新时间:2023-10-16

问题是使用std::list实现0(1)擦除列表项的推荐方法是什么?

通常,当我选择一个双链表时,我希望能够在O(1)时间内从一个列表中移除一个元素,然后在O(1)时间内将其移动到另一个列表中。如果元素有自己的prevnext指针,则没有真正的技巧来完成这项工作。如果链表是双链循环链表,则删除该链表并不一定需要知道包含该项的链表。

根据迭代器失效规则,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::liststd::unordered_map,为指针类型创建一个专门的列表。这是更重量级的(因为哈希表),但是提供了一个在接口级别非常接近标准容器的容器,并且给我O(1)个列表元素擦除。稻草人提案中唯一缺少的特性是指向当前包含该项的列表的指针。我已经在CodeReview上发布了当前的实现以征求意见。

std::list::erase保证为0(1)。

从标准列表中删除元素的方法并不多。(std::list::remove和朋友不做完全相同的事情,所以他们不计算)。

如果要从标准列表中擦除,则需要迭代器和列表本身。你好像已经有了。没有太多的自由去做不同的事情。我会将列表包含与对象分开,不像您所做的那样,因为为什么要创建一次只能在一个列表中的对象呢?对我来说这是不必要的人为限制。但无论你的设计有什么动力。

也许您可以重新设计您的接口,以提供迭代器而不是原始对象?在计时器的例子中:

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));

无可否认,这有点麻烦,但其他选择也是如此。

不可能从std::list中进行0(1)擦除。

您可以考虑使用intrusive list,其中列表节点直接嵌入到结构中,就像您已经做过的那样。

你可以使用boost:: intrusion或滚动你自己的,也看看这个

这是一个使用嵌入式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的迭代器类型的helper模板的实现。它的不同之处在于,*->操作符的定义方式使迭代器的行为像T *而不是std::shared_ptr<T> *(这是底层迭代器通常会做的)。

template <typename T>
class List<T>::Item {
    friend class List<T>;
    mutable typename Traits::Ref ref_;
};

List<T>::Item派生的T类型可以添加到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();
    }
}

这段代码说明了记忆是如何执行的。在执行push_front()时,首先检查该项是否已经包含。如果不是,则插入它,并将结果迭代器添加到ref_对象中。否则,如果它不是前面的元素,则删除该元素并将其重新插入到前面,并更新记忆迭代器。pop_front()移除记忆迭代器,然后在std::list上调用pop_front()

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()例程获取记忆迭代器,并使用它执行擦除操作。

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();
}

由于项目在很多方面都是它自己的迭代器,因此在List的这个版本中不需要find()方法。但是,代替它的是contains()方法,该方法用于查看元素是否为列表的成员。

现在,给出的解决方案使用std::map将列表实例关联到迭代器。如果一个项目同时属于的列表数量相对较少,这就保持了O(1)的精神。

我将尝试我的手在boost::intrusive版本下。

可怕的事实:虽然链表是一个强大的结构,std::list不能充分利用它的能力。

你不能只使用迭代器从std::list中删除对象,因为list必须释放节点,并且你必须知道分配内存的分配器在哪里(提示:它在列表中)。

与标准链表相比,侵入式容器有很多优点,比如自我意识;-),按值存储多态对象的能力,它们使列表技巧(比如在多个列表中拥有单个对象)变得可行。既然您不直接使用std::list,那么您可以完全放弃使用std::list并使用第三方容器,或者使用您自己的容器。

(同样,您的解决方案也是侵入性的,因为您的类型必须从List<T>::Item派生,这对类型施加了std::list没有的某些要求)