Подтвердить что ты не робот

Устойчива ли конструкция std:: remove и std:: remove_if?

Недавно (из одного комментария SO) я узнал, что std::remove и std:remove_if являются стабильными. Я ошибаюсь, думая, что это ужасный выбор дизайна, поскольку он предотвращает определенные оптимизации?

Представьте себе удаление первого и пятого элементов 1M std::vector. Из-за стабильности мы не можем реализовать remove с помощью swap. Вместо этого мы должны сдвигать каждый оставшийся элемент.:(

Если бы мы не были ограничены стабильностью, мы могли бы (для RA и BD iter) иметь практически 2 итератора, один спереди, второй сзади, а затем использовать swap для вывода объектов, которые нужно удалить. Я уверен, что умные люди могли бы сделать еще лучше. Мой вопрос в целом, а не о конкретной оптимизации, о которой я говорю.

РЕДАКТИРОВАТЬ: обратите внимание, что С++ рекламирует принцип нулевой надбавки, а также есть алгоритмы сортировки std::sort и std::stable_sort.

EDIT2: оптимизация будет выглядеть примерно так:

Для remove_if:

  • bad_iter смотрит с самого начала для тех элементов, для которых предикат возвращает true.
  • good_iter смотрит с конца на те элементы, для которых предикат возвращает false.

когда оба нашли то, что ожидается, они поменяют свои элементы. Терминация составляет good_iter <= bad_iter.

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

EDIT3: Я играл и пытался найти худший случай (худший случай для remove_if - обратите внимание, как редко предикат будет правдой), и я получил это:

#include <vector>
#include <string>
#include <iostream>
#include <map>
#include <algorithm>
#include <cassert>
#include <chrono>
#include <memory>
using namespace std;
int main()
{  
    vector<string> vsp;
    int n;
    cin >> n;
    for (int i =0; i < n; ++i)
    {   string s = "123456";
        s.push_back('a' + (rand() %26));
        vsp.push_back(s);
    }
    auto vsp2 = vsp;
    auto remove_start = std::chrono::high_resolution_clock::now();
    auto it=remove_if(begin(vsp),end(vsp), [](const string& s){ return s < "123456b";});
    vsp.erase(it,vsp.end());
    cout << vsp.size() << endl;
    auto remove_end = std::chrono::high_resolution_clock::now();
    cout << "erase-remove: " << chrono::duration_cast<std::chrono::milliseconds>(remove_end-remove_start).count() << " milliseconds\n";

    auto partition_start = std::chrono::high_resolution_clock::now();
    auto it2=partition(begin(vsp2),end(vsp2), [](const string& s){ return s >= "123456b";});
    vsp2.erase(it2,vsp2.end());
    cout << vsp2.size() << endl;
    auto partition_end = std::chrono::high_resolution_clock::now();
    cout << "partition-remove: " << chrono::duration_cast<std::chrono::milliseconds>(partition_end-partition_start).count() << " milliseconds\n";
}



C:\STL\MinGW>g++ test_int.cpp -O2 && a.exe
12345678
11870995
erase-remove: 1426 milliseconds
11870995
partition-remove: 658 milliseconds

Для других применений раздел бит быстрее, то же или медленнее. Цвет меня озадачен.: D

4b9b3361

Ответ 1

Я предполагаю, что вы спрашиваете о гипотетическом определении stable_remove как о том, что remove в настоящее время, и remove для реализации, однако разработчик считает, что лучше всего давать правильные значения в любом порядке. С ожиданием, что разработчики смогут улучшить только то же самое, что и stable_remove.

На практике библиотека не может легко сделать эту оптимизацию. Это зависит от данных, но вы не хотите тратить слишком много времени, чтобы определить, сколько элементов будет удалено, прежде чем принимать решение о том, как удалить их. Например, вы можете сделать дополнительный проход, чтобы посчитать их, но есть много случаев, когда этот дополнительный проход неэффективен. Просто потому, что неустойчивое удаление быстрее, чем стабильное для определенных случаев, не обязательно означает, что адаптивный алгоритм выбора между ними - хорошая ставка.

Я думаю, что разница между remove и sort заключается в том, что сортировка известна как сложная проблема с множеством различных решений и компромиссов и настроек. Все "простые" алгоритмы сортировки в среднем медленны. Большинство стандартных алгоритмов довольно просты, а remove - один из них, но sort - нет. Поэтому я не думаю, что поэтому имеет смысл определить stable_remove и remove как отдельные стандартные функции.

Изменить: ваше редактирование с помощью моей настройки (похоже на std::partition, но не нужно сохранять значения справа) кажется мне довольно разумным. Для этого требуется двунаправленный итератор, но в стандарте для алгоритмов, которые ведут себя по-разному в разных категориях итератора, есть прецедент, например std::distance. Таким образом, можно было бы определить стандарт unstable_remove, который требует только итератора forward, но делает вашу вещь, если он получает итератор bidi. Стандарт, вероятно, не выложил бы алгоритм, но он мог бы иметь такую ​​фразу, как "если итератор двунаправлен, делает не более min(k, n-k) перемещение, где k - количество удаленных элементов", что на самом деле заставило бы его, Но обратите внимание, что стандарт в настоящее время не говорит, сколько ходов remove_if делает, поэтому я считаю, что это ограничение не было приоритетом.

Конечно, ничто не мешает вам реализовать свой собственный unstable_remove.

Если мы согласны с тем, что стандарту не нужно указывать неустойчивое удаление, тогда возникает вопрос, следует ли назвать функцию, которую он определяет, stable_remove, ожидая будущего remove, который ведет себя по-разному для bidi итераторы и могут вести себя по-разному для передовых итераторов, если какая-то умная эвристика для выполнения неустойчивого удаления когда-либо становится достаточно известной, как известно, стоит стандартной функции. Я бы сказал, нет: это не катастрофа, если имена стандартных функций не являются полностью регулярными. Было бы довольно сложно устранить гарантию стабильности от STL remove_if. Затем возникает вопрос: "Почему STL не назвал его stable_remove_if", на который я могу ответить только в том, что в дополнение ко всем пунктам, сделанным во всех ответах, процесс проектирования STL был быстрее, чем процесс стандартизации.

stable_remove также откроет банку червей относительно других стандартных функций, которые теоретически могут иметь нестабильные версии. Для особо глупого примера следует copy называть stable_copy, на всякий случай, если существует некоторая реализация, на которой она явно быстрее меняет порядок элементов при копировании? Должен ли copy называться copy_forward, так что реализация может выбрать, какой из copy_backward и copy_forward вызывается copy, согласно которому быстрее? Часть работы комитета состоит в том, чтобы провести линию где-то.

Я считаю, что реальный стандарт разумный, и было бы разумно отдельно определять stable_remove и remove_with_some_other_constraints, но remove_in_some_unspecified_way просто не дает той же возможности для оптимизации, что и sort_in_some_unspecified_way. Introsort был изобретен в 1997 году, так же, как С++ был стандартизирован, но я не думаю, что исследовательская работа вокруг remove - это то, что было и есть около sort. Возможно, я ошибаюсь, оптимизация remove может быть следующей большой вещью, и если так, то комитет упустил трюк.

Ответ 2

std::remove указан для работы с итераторами вперед.

Подход с работой с парой итераторов от начала и до конца будет либо увеличивать требования к итераторам, а тем самым уменьшать полезность функции или нарушать/ухудшать асимптотические сложности.

Ответ 3

Отвечать на мой вопроs > 3 года спустя:)
Да, это был "провал".

Существует предложение D0041R0, которое добавит неустойчивое_remove. Можно утверждать, что только потому, что есть предложение добавить std:: unstable_remove, что это не означает, что std:: remove был ошибкой, но я не согласен.:)