Почему обратная функция для std::list
класса в стандартной библиотеке C ++ имеет линейное время выполнения? Я думаю, что для двусвязных списков обратная функция должна была быть O (1).
Изменение двусвязного списка должно включать переключение указателей головы и хвоста.
c++
c++11
stl
linked-list
любознательный
источник
источник
Reverse
функция будет реализована в O (1)?Ответы:
Гипотетически,
reverse
мог быть O (1) . Там (опять же гипотетически) мог быть элемент булевого списка, указывающий, является ли направление связанного списка в настоящее время таким же или противоположным, чем исходное, где был создан список.К сожалению, это снизит производительность практически любой другой операции (хотя и без изменения асимптотического времени выполнения). В каждой операции необходимо будет обращаться к логическому значению, чтобы определить, следует ли следовать указателю «следующий» или «предыдущий» ссылки.
Поскольку это, по-видимому, считалось относительно редкой операцией, стандарт (который не диктует реализации, а только сложность) указывал, что сложность может быть линейной. Это позволяет указателям «следующий» всегда однозначно обозначать одно и то же направление, что ускоряет выполнение обычных операций.
источник
reverse
сO(1)
сложностью , не затрагивая большой-о какой - либо другой операции , с помощью этого булева трюк флага. Но на практике дополнительная ветвь в каждой операции обходится дорого, даже если технически это O (1). Напротив, вы не можете создать структуру списка, в которойsort
O (1) и все другие операции имеют одинаковую стоимость. Суть вопроса в том, что, похоже, вы можете получитьO(1)
реверс бесплатно, если вы заботитесь только о большом О, так почему же они этого не сделали.std::uintptr_t
. Затем вы можете xor их.std::uintptr_t
, вы можете привести кchar
массиву, а затем XOR компонентов. Это будет медленнее, но на 100% портативно. Вероятно, вы могли бы сделать выбор между этими двумя реализациями и использовать в качестве запасного только второе, еслиuintptr_t
оно отсутствует. Некоторые, если это описано в этом ответе: stackoverflow.com/questions/14243971/…Это может быть O (1), если список будет хранить флаг, который позволяет менять значение указателей «
prev
» и «next
» каждого узла. Если обращение к списку будет частой операцией, такое добавление может быть действительно полезным, и я не знаю ни одной причины, по которой его реализация будет запрещена действующим стандартом. Однако наличие такого флага сделало бы обычный обход списка более дорогим (хотя бы с постоянным коэффициентом), потому что вместов
operator++
итераторе списка вы получитеэто не то, что вы решили бы добавить легко. Учитывая , что списки обычно проходится гораздо чаще , чем они поменялись местами, было бы очень неразумно стандарт для санкционировать эту технику. Следовательно, обратная операция может иметь линейную сложность. Заметим, однако, что t ∈ O (1) ⇒ t ∈ O ( n ), поэтому, как упоминалось ранее, техническая реализация вашей «оптимизации» будет разрешена.
Если вы пришли из Java или из аналогичного фона, вы можете спросить, почему итератор должен каждый раз проверять флаг. Не могли бы мы вместо этого иметь два разных типа итераторов, оба производных от общего базового типа, а также иметь
std::list::begin
иstd::list::rbegin
полиморфно возвращать соответствующий итератор? Хотя это и возможно, это еще больше ухудшит ситуацию, поскольку продвижение итератора теперь будет косвенным (трудно встроенным) вызовом функции. В Java вы все равно платите эту цену регулярно, но опять же, это одна из причин, по которой многие люди обращаются к C ++, когда производительность критична.Как отметил Бенджамин Линдли в комментариях, поскольку
reverse
недопустимо делать недействительными итераторы, единственным подходом, допускаемым стандартом, кажется, является сохранение указателя на список внутри итератора, который вызывает двойной косвенный доступ к памяти.источник
std::list::reverse
не делает недействительными итераторы.next
иprev
указатели в массиве, и сохраняет направление как0
или1
. Чтобы выполнить итерацию вперед, вы должны следоватьpointers[direction]
и выполнять итерацию в обратном направленииpointers[1-direction]
(или наоборот). Это все равно добавит немного накладных расходов, но, вероятно, меньше, чем ветвь.swap()
указано постоянное время и не делает недействительными никакие итераторы.Конечно, поскольку все контейнеры, поддерживающие двунаправленные итераторы, имеют концепцию rbegin () и rend (), этот вопрос является спорным?
Тривиально создать прокси, который переворачивает итераторы и получает доступ к контейнеру через него.
Эта неоперация действительно O (1).
Такие как:
ожидаемый результат:
Учитывая это, мне кажется, что комитет по стандартизации не потратил время на обязательное обратное упорядочение O (1) контейнера, потому что это не является необходимым, и стандартная библиотека в значительной степени построена на принципе обязательного выполнения только того, что строго необходимо, в то время как избегая дублирования.
Просто мой 2с.
источник
Потому что он должен пройти через каждый узел (
n
всего) и обновить их данные (шаг обновления действительноO(1)
). Это делает всю операциюO(n*1) = O(n)
.источник
Он также меняет предыдущий и следующий указатель для каждого узла. Вот почему это занимает Линейный. Хотя это можно сделать в O (1), если функция, использующая этот LL, также принимает информацию о LL в качестве входных данных, например, обращается ли она нормально или наоборот.
источник
Только объяснение алгоритма. Представьте, что у вас есть массив с элементами, тогда вам нужно инвертировать его. Основная идея состоит в том, чтобы выполнить итерацию для каждого элемента, меняя элемент в первой позиции на последнюю позицию, элемент во второй позиции на предпоследнюю позицию и так далее. Когда вы достигнете середины массива, все элементы будут изменены, таким образом, за (n / 2) итераций, что считается O (n).
источник
Это O (n) просто потому, что нужно скопировать список в обратном порядке. Каждой отдельной операции элемента является O (1), но во всем списке их n.
Конечно, существуют некоторые операции с постоянным временем, связанные с установкой пространства для нового списка, последующим изменением указателей и т. Д. Нотация O не учитывает отдельные константы, если вы включите n-фактор первого порядка.
источник