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

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

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

Ответ 1

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

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

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

Ответ 2

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

current = current->next;

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

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

который вы не можете легко добавить. Учитывая, что списки, как правило, проходят гораздо чаще, чем их отменять, было бы очень неразумно, если бы стандарт применял этот метод. Поэтому для обратной работы допускается линейная сложность. Обратите внимание, однако, что t & isin; O (1) → t & isin; O (n), поэтому, как упоминалось ранее, техническая поддержка "оптимизации" будет разрешена.

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

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

Ответ 3

Конечно, поскольку все контейнеры, поддерживающие двунаправленные итераторы, имеют понятие 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.

Ответ 4

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

Ответ 5

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

Ответ 6

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

Ответ 7

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

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