如何从std :: list中实现O(1)擦除

时间:2013-04-18 00:21:10

标签: c++ c++11 stdlist

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

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

根据Iterator invalidation rulesstd::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提出当前的实施,以征求意见。

6 个答案:

答案 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个相邻节点的前向和后向指针设置为相互指向来自行删除。