Где я могу получить достойное высокоуровневое описание алгоритма, используемого в __merge_without_buffer() в C++ STL? Я пытаюсь переопределить этот код на языке программирования D с некоторыми улучшениями. Я не могу понять, что он делает на алгоритмическом уровне, просто прочитав исходный код STL, потому что слишком много низкоуровневых деталей скрывают его. Кроме того, я не хочу просто слепо переводить код, потому что тогда, если бы это не сработало, я бы не знал, почему, и я не смог бы добавить свои улучшения.





__merge_without_buffer() выполняет слияние на месте как этап слияния сортировки слиянием на месте. Он принимает в качестве входных данных два диапазона данных [first, middle) и [middle, last), которые, как предполагается, уже отсортированы. Параметры len1 и len2 равны длинам двух входных диапазонов, а именно (middle - first) и (last - middle) соответственно.
Сначала он выбирает элемент вращаться. Затем он перестраивает данные в A1 B1 A2 B2 порядке, где A1 есть множество элементов в [first, middle), которые меньше, чем стержень, A2 представляет собой набор элементов в [first, middle) больше или равно к оси поворота, B1 есть множество элементов в [middle, last) меньше точки поворота, а B2 - это набор элементов в [middle, last), больший или равный оси поворота. Обратите внимание, что данные изначально находятся в порядке A1 A2 B1 B2, поэтому все, что нам нужно сделать, это превратить A2 B1 в B1 A2. Это при вызове std::rotate(), который делает именно это.
Теперь мы отделили элементы, которые меньше точки поворота (A1 и B1), от тех, которые больше или равны оси поворота (A2 и B2), поэтому теперь мы можем рекурсивно объединить два поддиапазона A1 A2 и B1 B2.
Как выбрать точку опоры? В рассматриваемой мной реализации он выбирает медианный элемент из большего поддиапазона (т.е. если [first, middle) имеет больше элементов, чем [middle, last), он выбирает медианное значение [first, middle); в противном случае он выбирает медианное значение [middle, last)). Поскольку поддиапазоны уже отсортированы, выбор медианы тривиален. Этот выбор поворота гарантирует, что при рекурсивном объединении двух поддиапазонов, каждая подзадача является не более чем 3/4 от размера текущей проблемы, потому что в худшем случае, по крайней мере 1/4 элементов больше или меньше, чем стержень .
Какое время работы у этого? Вызов std::rotate() занимает O (N) времени, и мы делаем два рекурсивных вызова себе. Это соответствует времени работы O (N log N). Однако обратите внимание, что это только один шаг в сортировке слиянием: помните, что в сортировке слиянием вы сначала рекурсивно сортируете обе половины, а затем объединяете. Таким образом, рекуррентное соотношение для времени выполнения сортировки слиянием теперь выглядит следующим образом:
T(N) = 2T(N/2) + O(N log N)
Подключите это к Основная теорема, и вы получите, что сортировка слиянием теперь выполняется за время O (N log2 N)!
В качестве последнего интересного момента рассмотрим следующие три качества алгоритма сортировки на основе сравнения:
Обычно вы можете получить только 2 из них одновременно - быстрая сортировка дает вам (1) и (3), слияние дает вам (2) и (3), а сортировка слияния на месте дает вам (1) и (2). Сортировки, не основанные на сравнении, такие как сортировка по счетчику, могут обеспечить все 3, но эти сортировки могут сортировать только определенные типы данных. Возможно, существует сортировка на основе сравнения, которая выполняет все три, но если есть, я не знаю о ее существовании, и это почти наверняка намного сложнее.
Я отредактировал, чтобы ответить на ваши вопросы. Я испортил надстрочный тег HTML. log ^ 2 N означает (log N) ^ 2. Хотя подразумевается основание 2, фактическое основание не имеет значения, поскольку это постоянный коэффициент, который съедается нотацией большого O.
В пункте 3, я думаю, вы имеете в виду, что поддиапазоны A1 и B1 рекурсивно объединяются, а поддиапазоны A2 и B2 рекурсивно объединяются, верно? После вращения поддиапазона A1 A2 больше нет.
Спасибо, но теперь, когда я знаю, что это O (N log N), а не O (N), я даже не уверен, что хочу больше беспокоиться о его реализации, хотя я думаю, что это действительно крутой алгоритм, поэтому я мог бы просто посмотреть, насколько медленно это на практике.
Обратите внимание, что он вызывается только тогда, когда у вас мало памяти. Когда вызывающие абоненты могут выделить временный буфер, они не вызывают версию на месте. Вместо этого они называют O (n) «адаптивной» версией. Когда он находится между «медленным» и «неудачным», выбирайте первое.
Стандартная быстрая сортировка не на месте, она требует непостоянного дополнительного стека. Существует модифицированная быстрая сортировка, которая работает в постоянной дополнительной памяти (сохраняя свой стек в массиве с помощью хитрых приемов), но она довольно неэффективна, хотя по-прежнему O (n * log (n)).
Верно, но для большинства случаев использования в реальном мире стек O (log N) можно считать "почти" установленным. Он не создает активности кучи и имеет минимальный риск переполнения стека.
В конечном итоге я реализовал это, чтобы удовлетворить свое любопытство, и это не так медленно, как я думал. Спасибо.
Между прочим, я отправил сообщение Адаму Розенфилду за пределы сайта и подумал, что некоторые из вас хотели бы знать. С точки зрения теории, действительно существуют алгоритмы, обладающие «всеми тремя хорошими качествами». См. portal.acm.org/citation.cfm?id=892042 и springerlink.com/content/bx6d9xv8p6w3d3p4.
Да, я немного посмотрел на них. Они кажутся невероятно сложными для понимания и реализации, хотя я бы хотел, чтобы одна работала. Я предполагаю, что есть причина, по которой они не в STL.
Означает ли O (N log2 N) N (log N) ^ 2? Я был сбит с толку, потому что думал, что log2 означает log_2, но база здесь не имеет значения. Кроме того, является ли последняя часть «вы можете получить только 2 из них одновременно» строго верной, или просто алгоритмы, которые получают все три, являются сложными? Думаю, второе?