Как добиться стирания O(1) из std::list

Вопрос в том, каков рекомендуемый способ использования std::list для достижения O (1) стирания элементов списка?

Обычно, когда я выбираю двусвязный список, я хочу иметь возможность удалить элемент из списка за время O(1), а затем переместить его в другой список за время O(1). Если у элемента есть собственные указатели prev и next, нет никакой реальной хитрости, чтобы выполнить работу. Если список представляет собой двусвязный циклический список, то удаление не обязательно требует знания списка, содержащего элемент.

Согласно правилам аннулирования итераторов, 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).

Каков рекомендуемый способ стирания O (1) с помощью std::list? Или есть лучший подход к достижению цели?


В прошлом я добивался этого, реализуя собственную структуру данных списка, в которой элемент, помещенный в список, имеет собственные указатели next и prev. Управление этими указателями естественно, поскольку они присущи самим операциям со списками (API для моей реализации списка настраивает указатели). Если я хочу вместо этого использовать STL, как лучше всего это сделать? Я предлагаю соломенное предложение по внедрению итератора. Есть ли лучшие подходы?

Если желателен конкретный вариант использования, рассмотрите реализацию таймера. Когда таймер создан, он помещается в соответствующий список. Если он отменен, желательно его оперативно удалить. (Этот конкретный пример можно решить с помощью маркировки вместо удаления, но это допустимый способ реализации отмены.) Дополнительные варианты использования доступны по запросу.


Другой альтернативой, которую я исследовал, было объединение std::list с std::unordered_map для создания специализированного списка типов указателей. Это более тяжеловесно (из-за хеш-таблицы), но предоставляет контейнер, который довольно близок к стандартным контейнерам на уровне интерфейса, и дает мне стирание O(1) элементов списка. Единственная функция, отсутствующая в предложении соломенного человечка, — это указатель на список, который в настоящее время содержит элемент. Я разместил текущую реализацию на CodeReview, чтобы получить комментарии.


person jxh    schedule 18.04.2013    source источник
comment
Просто подумай об этом. Вы никогда не сможете получить доступ к случайному элементу реализации связанного списка за время O (1), это всегда будет O (n). Только голова и хвост могут быть доступны в течение постоянного времени. Тот факт, что вам нужно иметь доступ к определенному элементу (произвольный доступ), просто предполагает, что вы используете неправильный контейнер, и вам следует выбрать vector или что-то подобное. С моей точки зрения, создавать такие тяжеловесные обертки overkill и неэффективно (с точки зрения памяти и производительности), и непрактично.   -  person Alexander Shukaev    schedule 18.04.2013
comment
Итак, узел в списке хочет удалить себя из списка?   -  person cppguy    schedule 18.04.2013
comment
@cppguy: Короче говоря, да.   -  person jxh    schedule 18.04.2013
comment
Загляните в навязчивые контейнеры Boost, это больше похоже на то, что вы хотите.   -  person GManNickG    schedule 18.04.2013
comment
@Haroogan: Если вы имеете в виду контейнер получше, я открыт, но он должен иметь ту же пространственную и временную сложность, что и std::list. Мое решение в прошлом состояло в том, чтобы реализовать мою собственную структуру данных списка, чтобы разрешить мне это свойство. Я ищу способы добиться того же свойства с помощью STL.   -  person jxh    schedule 18.04.2013
comment
да, извини, понял, что пропустил этот момент. Конечно, это невозможно сделать, если ваш объект сам по себе не поддерживает ссылки. В противном случае у вас всегда будет что-то, указывающее на ваш объект и следующие/предыдущие узлы. При этом ваш объект должен иметь адрес для своего собственного контейнера, или вы должны искать его каждый раз. Решение состоит в том, чтобы управлять связанным списком внутри ваших объектов, если это целесообразно.   -  person Dave    schedule 18.04.2013
comment
@ user315052: Ни один контейнер не имеет такой же пространственной и временной сложности, кроме самого std::list :). Вам в любом случае придется от чего-то отказаться, и вы должны хорошенько подумать, что это такое в контексте вашей проблемы. Я не могу предложить ничего особенного прямо сейчас, потому что я не знаю, зачем вам это нужно.   -  person Alexander Shukaev    schedule 18.04.2013
comment
Вы пробовали просто использовать std::map?   -  person derpface    schedule 18.04.2013
comment
@uberwulu: Ключа как такового нет. Кроме того, операции не O (1).   -  person jxh    schedule 18.04.2013
comment
@Haroogan: я действительно ни от чего не отказываюсь, если хочу встроить итератор. Мне просто нужно написать большую оболочку, чтобы управлять ею для каждой операции со списком. Вы говорите, что это не стоит усилий, чтобы сделать это. Вы по-прежнему считаете, что это правда, если альтернатива состоит в том, чтобы полностью реализовать мой собственный список?   -  person jxh    schedule 18.04.2013
comment
Если вы храните итератор для каждого элемента внутри элемента, вы можете вызвать cplusplus .com/reference/list/list/erase. сложность стирания O(n), где n — количество удаленных элементов. И да, std::list — это двусвязный список. Но ссылки на расположение внутри списка не должны храниться вне списка. это работа списка знать эти вещи, а не ваша.   -  person scones    schedule 18.04.2013
comment
Во-первых, я до сих пор не понимаю, почему вам так срочно нужен связанный список, зная, что его объем памяти значительно выше, чем, например, в vector. Во-вторых, поскольку его элементы фактически разбросаны по памяти (не образуют непрерывный блок, как это делает vector), промахи кеша, вероятно, составят 99%, что означает, что он будет медленным, нет, действительно будет медленным. Вы понимаете все эти детали? Поэтому я еще раз спрашиваю вас, вы абсолютно уверены, что вам нужен связанный список с дополнительной тяжелой оболочкой над ним (что еще больше снизит эффективность)?   -  person Alexander Shukaev    schedule 18.04.2013
comment
Кроме того, я полагаю, вы понимаете, что те итераторы, которые вы собираетесь сохранить в своей оболочке, будут недействительны, как только список изменится. Что бы вы сделали по этому поводу?   -  person Alexander Shukaev    schedule 18.04.2013
comment
@Haroogan: Не вдаваясь в подробности, давайте просто предположим, что я уже решил проблему местоположения объекта. Что заставляет вас верить, что обертка будет такой тяжелой? Как я уже говорил в своем вопросе, итераторы списков очень надежны (очень трудно признать недействительными).   -  person jxh    schedule 18.04.2013
comment
Это просто прямое следствие. Предположим, что новый элемент вставлен (или удален) в список рядом с тем, что делает ваша оболочка в этой ситуации? Пересчитать итераторы для всех Item?   -  person Alexander Shukaev    schedule 18.04.2013
comment
@ Haroogan: это ошибочное предположение. Такой перерасчет не требуется.   -  person jxh    schedule 18.04.2013
comment
Да, извините, забыл об этом. Теперь я вижу вашу точку зрения там. Тогда последний вопрос: имеют ли объекты, которые вы хотите сохранить в list, размер >>, чем от 8 до 16 байт? Я до сих пор не знаю ваших требований к памяти.   -  person Alexander Shukaev    schedule 18.04.2013
comment
@Haroogan: около 64 байтов на объект для наиболее распространенных случаев, но список используется в общем.   -  person jxh    schedule 18.04.2013
comment
Что ж, похоже, вы будете вкладывать дополнительные 50% в метаинформацию, что может оказаться существенным, если вы стремитесь манипулировать огромными list. Но если это не так, я думаю, вас устроит предложенное вами решение. Я до сих пор не знаю, как вы справились с локальностью данных, с list это кажется невозможным. Даже если вы храните эти объекты в непрерывном массиве и сохраняете указатели на эти объекты в list (а не сами объекты), локальность все равно нарушается.   -  person Alexander Shukaev    schedule 18.04.2013
comment
@Haroogan: Поскольку мне нужно переместить объект из одного списка в другой, это указатель, как указано в моем предложении. Таким образом, массивы будут иметь ту же проблему.   -  person jxh    schedule 18.04.2013
comment
Я вижу, как что-то подобное может быть полезным. Однако, может быть, вы могли бы изменить свой API, чтобы выдавать клиентам итераторы, а не сами объекты? Если вы наберете его, клиенту даже не нужно будет знать, что это итератор; вы можете просто сказать, что это объект, похожий на указатель.   -  person Thomas    schedule 21.04.2013
comment
@Thomas: кажется, я понимаю, но не могли бы вы предоставить грубый набросок кода идеи в ответе?   -  person jxh    schedule 21.04.2013


Ответы (5)


std::list::erase гарантированно будет O(1).

Существует не так много других способов стереть элементы из стандартного списка. (std::list::remove и друзья не делают одно и то же, поэтому не учитываются).

Если вы хотите удалить из стандартного списка, вам нужен итератор и сам список. Это то, что у вас, кажется, уже есть. Существует не очень много свободы делать это по-другому. Я бы сохранил включение списка отдельно от объектов, в отличие от того, что вы сделали, потому что зачем создавать объект, который может быть только в одном списке за раз? Мне кажется ненужным искусственным ограничением. Но что бы ни приводило в действие ваш дизайн.

person n. 1.8e9-where's-my-share m.    schedule 18.04.2013
comment
Я предоставил полное решение в ответе, и оно касается вашей последней проблемы. - person jxh; 21.04.2013

Может быть, вы могли бы изменить свой интерфейс, чтобы выдавать итераторы вместо необработанных объектов? В случае вашего примера таймеров:

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

По общему признанию, это немного громоздко, но альтернативы тоже.

person Thomas    schedule 21.04.2013
comment
Понятно, но с таким подходом я не могу переместить таймер из одного списка в другой. О, я вижу, мне придется копировать базовые данные при переходе к другому списку. Может быть сложно. Действительны ли данные, когда итератор используется для удаления элемента из списка? - person jxh; 21.04.2013
comment
Можно сделать; см. редактирование. По сути, хитрость заключается в том, чтобы различать типы свободно плавающего таймера (Timer) и таймера где-то в списке (TimerRef). - person Thomas; 21.04.2013
comment
Другие проблемы, которые у меня есть, незначительны, но последняя сложная проблема заключается в том, как клиент, владеющий итератором, узнает, что он недействителен (скажем, потому, что список, к которому он принадлежал, был уничтожен)? - person jxh; 21.04.2013
comment
Это не так. Опять же, если список был уничтожен, у клиента больше не должно быть никаких указателей/ссылок на него, поэтому он все равно не может даже пытаться использовать недопустимый итератор. - person Thomas; 21.04.2013
comment
Если бы Timers знал, что каждый Timer содержит итератор, он мог бы посетить каждого члена в своем списке и установить итератор каждого члена в end() или что-то в этом роде. Я полагаю, вы думаете, что мое полное решение ниже слишком тяжелое. - person jxh; 21.04.2013
comment
Не зная точной ситуации, я не могу решить, какое решение лучше. Я всего лишь предложил альтернативу. Если ваше тяжелое решение работает для вас, отлично :) - person Thomas; 21.04.2013

Невозможно стереть O(1) из std::list.

вы можете рассмотреть возможность использования intrusive list, где узлы списка непосредственно встроены в структуры, как вы уже сделали.

вы можете использовать ускорение: :intrusive или создайте свой собственный, также ознакомьтесь с это

person yngccc    schedule 18.04.2013
comment
Разве встраивание iterator в элемент не будет похоже на подход с назойливым списком и позволит стирать O (1)? - person jxh; 18.04.2013
comment
@ user315052 вы правы, ваш подход - O (1). навязчивый список может быть полезен, если вы хотите, чтобы несколько списков содержали один и тот же объект, но я также не вижу, чем он лучше, чем встроенный итератор. - person yngccc; 18.04.2013
comment
@yngum навязчивый список использует немного меньше памяти и требует на одно разыменование указателя меньше, что может быть полезно в некоторых случаях. Недостатком является дополнительная сложность. - person Dave; 18.04.2013
comment
@Dave: boost::intrusive::list кажется сложнее в использовании, потому что он внутренне хранит ссылки на помещаемый в него объект. Хотя это круто с точки зрения реализации, это усложняет управление памятью объектов. Это похоже на то, что у вас должен быть отдельный контейнер для самого объекта, прежде чем вы поместите его в boost::intrusive::list. - person jxh; 23.04.2013

Вот «полное» решение с использованием встроенного файла 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();
    }
}

Этот код иллюстрирует, как выполняется мемоизация. При выполнении push_front() элемент сначала проверяется, чтобы убедиться, что он уже содержится. Если это не так, он вставляется, а полученный итератор добавляется к объекту ref_. В противном случае, если он еще не находится впереди, элемент удаляется и снова вставляется впереди, а мемоизированный итератор обновляется. 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() извлекает запомненный итератор и использует его для выполнения стирания.

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.

person jxh    schedule 21.04.2013
comment
В классе List отсутствуют конструктор копирования/перемещения и методы оператора присваивания. - person jxh; 23.04.2013

Ужасная правда: хотя связанный список является мощной структурой, std::list не может полностью использовать его возможности.

Вы не можете стереть объект из std::list, используя только итераторы, потому что список должен освободить узел, и вы должны знать, где находится распределитель, который выделил память (подсказка: он находится в списке).

Навязчивые контейнеры имеют много преимуществ по сравнению со стандартным связанным списком, например самосознание ;-), способность хранить полиморфные объекты по значению и они делают возможными трюки со списками (например, наличие отдельных объектов в нескольких списках). Поскольку вы все равно не используете std::list напрямую, вы можете вообще отказаться от использования std::list и использовать сторонний контейнер или создать свой собственный.

(Кроме того, ваше решение также навязчиво, поскольку ваш тип должен быть получен из List<T>::Item, что налагает определенные требования на тип, которого нет в std::list)

person milleniumbug    schedule 21.04.2013