Почему std::list:: reverse имеет o(n) сложность?

почему обратная функция для std::list класс в стандартной библиотеке C++ имеет линейную среду выполнения? Я бы подумал, что для двусвязных списков обратная функция должна быть O(1).

реверсирование двусвязного списка должно включать просто переключение указателей головы и хвоста.

7 ответов


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

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

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


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

current = current->next;

на operator++ итератора списка, вы получите

if (reversed)
  current = current->prev;
else
  current = current->next;

что не то, что вы решили бы добавить легко. Учитывая, что списки обычно проходят гораздо чаще, чем они отменяются, было бы очень неразумно для стандарта мандат эта техника. Поэтому обратная операция может иметь линейную сложность. Заметьте, однако, что tO(1) ⇒ tO(n) таким образом, как упоминалось ранее, реализация вашей "оптимизации" технически будет разрешена.

если вы пришли из Java или аналогичного фона, вы можете задаться вопросом, почему итератор должен проверять флаг каждый раз. Не могли бы мы вместо этого иметь два разных типа итераторов, оба производные от общего базового типа, и иметь std::list::begin и std::list::rbegin полиморфно вернуть соответствующий итератор? Пока возможно, это сделало бы все еще хуже, потому что продвижение итератора было бы косвенным (трудно встроенным) вызовом функции сейчас. В Java вы все равно платите эту цену регулярно, но опять же, это одна из причин, по которой многие люди достигают C++, когда производительность критична.

как указал Бенджамин Линдли в комментариях, так как reverse Не разрешено аннулировать итераторы, единственным подходом, разрешенным стандартом, по-видимому, является хранение указатель назад к списку внутри итератора, который вызывает двойной косвенный доступ к памяти.


конечно, поскольку все контейнеры, поддерживающие двунаправленные итераторы, имеют концепцию rbegin () и rend (), этот вопрос спорный?

тривиально построить прокси, который отменяет итераторы и получает доступ к контейнеру через это.

это бездействие действительно O (1).

, например:

#include <iostream>
#include <list>
#include <string>
#include <iterator>

template<class Container>
struct reverse_proxy
{
    reverse_proxy(Container& c)
    : _c(c)
    {}

    auto begin() { return std::make_reverse_iterator(std::end(_c)); }
    auto end() { return std::make_reverse_iterator(std::begin(_c)); }

    auto begin() const { return std::make_reverse_iterator(std::end(_c)); }
    auto end() const { return std::make_reverse_iterator(std::begin(_c)); }

    Container& _c;
};

template<class Container>
auto reversed(Container& c)
{
    return reverse_proxy<Container>(c);
}

int main()
{
    using namespace std;
    list<string> l { "the", "cat", "sat", "on", "the", "mat" };

    auto r = reversed(l);
    copy(begin(r), end(r), ostream_iterator<string>(cout, "\n"));

    return 0;
}

ожидаемый результат:

mat
the
on
sat
cat
the

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

только мой 2c.


потому что он должен пересечь каждый узел (n total) и обновлять данные (шаг обновления действительно O(1)). Это делает всю операцию O(n*1) = O(n).


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


только объяснение алгоритма. Представьте, что у вас есть массив с элементами, тогда вам нужно перевернуть его. Основная идея состоит в том, чтобы перебирать каждый элемент, изменяя элемент на первая позиция до последней позиции, элемент на второй позиции до предпоследней позиции и так далее. Когда вы достигнете середины массива, все элементы будут изменены, таким образом, в(n/2) итерациях, которые считаются O (n).


Это O (n) просто потому, что ему нужно скопировать список в обратном порядке. Каждая операция отдельного элемента равна O (1), но во всем списке их n.

конечно, есть некоторые операции с постоянным временем, связанные с настройкой пространства для нового списка,а затем с изменением указателей и т. д. Нотация O не учитывает отдельные константы, как только вы включаете фактор N первого порядка.