Как реализуется nth_element?
В StackOverflow и в других местах есть много претензий, что nth_element
- O (n), и что он обычно реализуется с помощью Introselect: http://en.cppreference.com/w/cpp/algorithm/nth_element
Я хочу знать, как это может быть достигнуто. Я посмотрел на Википедию объяснение Introselect, и это просто оставило меня более смущенным. Как алгоритм переключается между QSort и Median-of-Medians?
Я нашел здесь статью Introsort: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.14.5196&rep=rep1&type=pdf Но это говорит:
В этой статье мы сосредоточимся на проблеме сортировки и кратко рассмотрим проблему выбора в следующем разделе.
Я попытался прочитать сам STL, чтобы понять, как nth_element
реализован, но он быстро завязывается.
Может ли кто-нибудь показать мне псевдокод для внедрения Introselect? Или даже лучше, реальный код на С++, отличный от STL, конечно:)
Ответы
Ответ 1
Вы задали два вопроса: титульный
Как реализуется nth_element?
Что вы уже ответили:
Существует много претензий к StackOverflow и в других местах, где nth_element является O (n) и что он обычно реализуется с помощью Introselect.
Что я также могу подтвердить, глядя на мою реализацию stdlib. (Подробнее об этом позже.)
И тот, где вы не понимаете ответа:
Как алгоритм может переключаться между QSort и Median-of-Medians?
Давайте посмотрим на псевдокод, который я извлек из своего stdlib:
nth_element(first, nth, last)
{
if (first == last || nth == last)
return;
introselect(first, nth, last, log2(last - first) * 2);
}
introselect(first, nth, last, depth_limit)
{
while (last - first > 3)
{
if (depth_limit == 0)
{
// [NOTE by editor] This should be median-of-medians instead.
// [NOTE by editor] See Azmisov comment below
heap_select(first, nth + 1, last);
// Place the nth largest element in its final position.
iter_swap(first, nth);
return;
}
--depth_limit;
cut = unguarded_partition_pivot(first, last);
if (cut <= nth)
first = cut;
else
last = cut;
}
insertion_sort(first, last);
}
Не вдаваясь в подробности о ссылочных функциях heap_select
и unguarded_partition_pivot
, мы можем ясно видеть, что nth_element
дает introselect 2 * log2(size)
шаги разбиения (в два раза больше, чем необходимо для быстрого выбора в лучшем случае), пока heap_select
пинает и решает проблему навсегда.
Ответ 2
Код STL (версия 3.3, я думаю):
template <class _RandomAccessIter, class _Tp>
void __nth_element(_RandomAccessIter __first, _RandomAccessIter __nth,
_RandomAccessIter __last, _Tp*) {
while (__last - __first > 3) {
_RandomAccessIter __cut =
__unguarded_partition(__first, __last,
_Tp(__median(*__first,
*(__first + (__last - __first)/2),
*(__last - 1))));
if (__cut <= __nth)
__first = __cut;
else
__last = __cut;
}
__insertion_sort(__first, __last);
}
Немного упростите это:
template <class Iter, class T>
void nth_element(Iter first, Iter nth, Iter last) {
while (last - first > 3) {
Iter cut =
unguarded_partition(first, last,
T(median(*first,
*(first + (last - first)/2),
*(last - 1))));
if (cut <= nth)
first = cut;
else
last = cut;
}
insertion_sort(first, last);
}
То, что я здесь делал, это удалить двойные символы подчеркивания и _Uppercase, которые предназначены только для защиты кода от того, что пользователь может юридически определить как макросы. Я также удалил последний параметр, который должен только помочь в выводе типа шаблона, и для краткости переименовал тип итератора.
Как вы теперь увидите, он многократно разбивает диапазон, пока в оставшемся диапазоне не останется меньше четырех элементов, которые затем просто сортируются.
Теперь, почему это O (n)? Во-первых, окончательная сортировка до трех элементов - это O (1) из-за максимума трех элементов. Теперь остается повторное разбиение. Разделение само по себе является O (n). Здесь каждый шаг уменьшает количество элементов, которые необходимо коснуться на следующем шаге, поэтому у вас есть O (n) + O (n/2) + O (n/4) + O (n/8), который равен меньше, чем O (2n), если вы суммируете его. Так как O (2n) = O (n), вы в среднем имеете сложность линейного текста.
Ответ 3
Отказ от ответственности: я не знаю, как std::nth_element
реализуется в любой стандартной библиотеке.
Если вы знаете, как работает Quicksort, вы можете легко изменить его, чтобы делать то, что необходимо для этого алгоритма. Основная идея Quicksort заключается в том, что на каждом шаге вы разбиваете массив на две части, так что все элементы, меньшие, чем точка поворота, находятся в левом суб-массиве, а все элементы, равные или превышающие опорные точки, находятся в правой поддиапазоне, (Модификация Quicksort известная как тройная Quicksort создает третий суб-массив со всеми элементами, равными осями поворота. Затем правый вспомогательный массив содержит только запись строго больше, чем стержень.), То Quicksort протекает рекурсивно сортировки левого и правого суб -arrays.
Если вы хотите только переместить n-й элемент на место, вместо того, чтобы рекурсировать в оба под-массива, вы можете сказать на каждом шаге, нужно ли вам спускаться в левый или правый под-массив. (Вы знаете это, потому что n-й элемент в отсортированном массиве имеет индекс n, поэтому он становится вопросом сравнения индексов.) Итак - если ваш Quicksort не пострадает от наихудшего вырождения - вы примерно вдвое уменьшаете размер оставшегося массива на каждом шаге, (Вы никогда больше не смотрите на другой вспомогательный массив.) Поэтому в среднем на каждом шаге вы имеете дело с массивами следующих длин:
- & Theta; (N)
- & Theta; (N/2)
- & Theta; (N/4)
- ...
Каждый шаг является линейным по длине массива, с которым он имеет дело. (Вы зацикливаете его один раз и решаете, в какой суб-массив каждый элемент должен идти в зависимости от того, как он сравнивается с точкой опоры.)
Вы можете видеть, что после шагов & Theta; (log (N)) мы в конечном итоге достигнем массива singleton и закончим. Если вы суммируете N (1 + 1/2 + 1/4 +...), вы получите 2 N. Или в среднем случае, так как мы не можем надеяться, что стержень всегда будет точно быть медианным, что-то на порядок & theta; (N).