Как эффективно remove_if только один элемент из forward_list?

Ну, я думаю, что вопрос в значительной степени подводит итог. У меня есть forward_list уникальных элементов и хочу удалить из него один элемент:

std::forward_list<T> mylist;
// fill with stuff

mylist.remove_if([](T const& value)
  {
    return value == condition;
  });

Я имею в виду, что этот метод работает нормально, но он неэффективен, потому что он продолжает поиск после того, как элемент найден и удален. Есть ли лучший способ или мне нужно сделать это вручную?

Ответ 1

Если вы хотите удалить только первое совпадение, вы можете использовать std::adjacent_find, а затем элемент erase_after

#include <algorithm>
#include <cassert>
#include <forward_list>
#include <iostream>
#include <ios>
#include <iterator>

// returns an iterator before first element equal to value, or last if no such element is present
// pre-condition: before_first is incrementable and not equal to last
template<class FwdIt, class T>
FwdIt find_before(FwdIt before_first, FwdIt last, T const& value)
{
    assert(before_first != last);
    auto first = std::next(before_first);
    if (first == last) return last;
    if (*first == value) return before_first;
    return std::adjacent_find(first, last, [&](auto const&, auto const& R) { 
        return R == value; 
    });
}

int main() 
{
    auto e = std::forward_list<int>{};
    std::cout << std::boolalpha << (++e.before_begin() == end(e)) << "\n";
    std::cout << (find_before(e.before_begin(), end(e), 0) == end(e)) << "\n";

    auto s = std::forward_list<int>{ 0 };
    std::cout << (find_before(s.before_begin(), end(s), 0) == s.before_begin()) << "\n";

    auto d = std::forward_list<int>{ 0, 1 };
    std::cout << (find_before(d.before_begin(), end(d), 0) == d.before_begin()) << "\n";
    std::cout << (find_before(d.before_begin(), end(d), 1) == begin(d)) << "\n";
    std::cout << (find_before(d.before_begin(), end(d), 2) == end(d)) << "\n";

    // erase after
    auto m = std::forward_list<int>{ 1, 2, 3, 4, 1, 3, 5 };
    auto it = find_before(m.before_begin(), end(m), 3);
    if (it != end(m)) 
        m.erase_after(it);
    std::copy(begin(m), end(m), std::ostream_iterator<int>(std::cout, ","));
}

Пример Live

Это прекратится, как только будет найдено совпадение. Обратите внимание, что adjacent_find принимает двоичный предикат, и, сравнивая только второй аргумент, мы получаем итератор перед элементом, который хотим удалить, так что erase_after может фактически удалить его. Сложность O(N), поэтому вы не получите ее более эффективной, чем это.

Ответ 2

FWIW, здесь еще одна короткая версия

template< typename T, class Allocator, class Predicate >
bool remove_first_if( std::forward_list< T, Allocator >& list, Predicate pred )
{
    auto oit = list.before_begin(), it = std::next( oit );
    while( it != list.end() ) {
        if( pred( *it ) ) { list.erase_after( oit ); return true; }
        oit = it++;
    }
    return false;
}

Ответ 3

В стандартной библиотеке нет ничего, что было бы непосредственно применимо. Собственно, есть. См. Ответ @TemplateRex для этого.

Вы также можете написать это самостоятельно (особенно если вы хотите совместить поиск с стиранием), примерно так:

template <class T, class Allocator, class Predicate>
bool remove_first_if(std::forward_list<T, Allocator> &list, Predicate pred)
{
  auto itErase = list.before_begin();
  auto itFind = list.begin();
  const auto itEnd = list.end();
  while (itFind != itEnd) {
    if (pred(*itFind)) {
      list.erase_after(itErase);
      return true;
    } else {
      ++itErase;
      ++itFind;
    }
  }
  return false;
}

Ответ 4

Приходится рулон...

template <typename Container, typename Predicate>
void remove_first_of(Container& container, Predicate p)
{
  auto it = container.before_begin();
  for (auto nit = std::next(it); ; it = nit, nit = std::next(it))
  {
    if (nit == container.end())
      return;
    if (p(*nit))
    {
      container.erase_after(it);
      return;
    }
  }
}

Более полный пример...

Ответ 5

Этот вид вещей был стандартным упражнением, когда я изучил программирование еще в начале 80-х. Было бы интересно вспомнить решение и сравнить его с тем, что можно сделать на С++. На самом деле это было в Алголе 68, но я не буду навязывать это вам и давать перевод на C. Учитывая

typedef ... T;
typedef struct node *link;
struct node { link next; T data; };

можно написать, понимая, что нужно передать адрес указателя заголовка списка, если можно отменить связь с первым node:

void search_and_destroy(link *p_addr, T y)
{
  while (*p_addr!=NULL && (*p_addr)->data!=y)
    p_addr = &(*p_addr)->next;
  if (*p_addr!=NULL)
  {
    link old = *p_addr;
    *p_addr = old->next; /* unlink node */
    free(old); /* and free memory */
  }
}

Здесь много событий *p_addr; он является последним, где это LHS присвоения, именно по этой причине нужен адрес указателя здесь, в первую очередь. Обратите внимание: несмотря на кажущееся усложнение, оператор p_addr = &(*p_addr)->next; просто заменяет указатель на значение, на которое указывает он, а затем добавляет смещение (которое здесь 0).

Можно ввести вспомогательное значение указателя, чтобы немного уменьшить код, как показано ниже.

void search_and_destroy(link *p_addr, T y)
{
  link p=*p_addr;
  while (p!=NULL && p->data!=y)
    p=*(p_addr = &p->next);
  if (p!=NULL)
  {
    *p_addr = p->next;
    free(p);
  }
}

но это принципиально тот же код: любой достойный компилятор должен понимать, что значение указателя *p_addr используется несколько раз подряд в первом примере и хранит его в регистре.

Теперь с std::forward_list<T> нам не разрешен доступ к указателям, которые связывают узлы, и получают эти неудобные "итераторы", указывающие один node перед реальным действием ". Наше решение становится

void search_and_destroy(std::forward_list<T> list, T y)
{
  std::forward_list<T>::iterator it = list.before_begin();
  const std::forward_list<T>::iterator NIL = list.end();

  while (std::next(it)!=NIL && *std::next(it)!=y)
    ++it;
  if (std::next(it)!=NIL)
    list.erase_after(it);
}

Снова мы могли бы сохранить вторую переменную итератора, чтобы удерживать std::next(it), не указывая ее каждый раз (не забывая обновлять ее значение, когда мы увеличиваем it) и получаем по существу ответ Дэниела Фрея. (Мы могли бы вместо этого попытаться сделать эту переменную указателем типа *T равным &*std::next(it) вместо этого, что достаточно для использования, которое мы делаем из нее, но на самом деле было бы немного хлопот, чтобы он стал нулевым указателем когда std::next(it)==NIL, так как стандарт не позволяет взять &*NIL).

Я не могу не почувствовать, что с давних времен решение этой проблемы не стало более элегантным.