Как реализуется N-й элемент?
есть много претензий на StackOverflow и в других местах, что nth_element
is O (n) и что он обычно реализуется с помощью Introselect:http://en.cppreference.com/w/cpp/algorithm/nth_element
Я хочу знать, как этого можно достичь. Я посмотрел на объяснение Википедии Introselect и это только что привело меня в еще большее замешательство. Как алгоритм может переключаться между QSort и медианой медианы?
Я нашел Introsort бумага Здесь:http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.14.5196&rep=rep1&type=pdf но это говорит:
в этой статье мы сосредоточимся на проблеме сортировки и вернемся к проблеме выбора только кратко в более позднем разделе.
Я попытался прочитать сам STL, чтобы понять, как nth_element
реализовано, но это становится волосатым очень быстро.
может кто-нибудь показать мне псевдо-код для того, как Introselect реализовано? Или даже лучше, фактический код c++, отличный от STL, конечно :)
3 ответов
вы задали два вопроса, титульный
как реализуется nth_element?
на что вы уже ответили:
есть много утверждений о StackOverflow и в других местах, что nth_element является O(n) и что он обычно реализуется с помощью Introselect.
что я также могу подтвердить, глядя на мою реализацию stdlib. (Подробнее об этом позже.)
и тот, где вы не понимаю, ответ:
как алгоритм может переключаться между QSort и медианой медианы?
давайте посмотрим на псевдо код, который я извлек из моей 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's 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)
шаги подразделения (в два раза больше, чем требуется quickselect в лучшем случае) до heap_select
умирает, и решает проблема навсегда.
отказ от ответственности: я не знаю, как std::nth_element
реализуется в любой стандартной библиотеке.
Если вы знаете, как работает Quicksort, вы можете легко изменить его, чтобы сделать то, что необходимо для этого алгоритма. Основная идея Quicksort заключается в том, что на каждом шаге вы разделяете массив на две части, так что все элементы, меньшие, чем ось, находятся в левом поддереве, а все элементы, равные или большие, чем ось, находятся в правом поддереве. (Модификация Quicksort, известная как тройная Quicksort создает третий под-массив со всеми элементами, равными оси вращения. Тогда правый под-массив содержит только записи, строго превышающие pivot.) Quicksort затем продолжается рекурсивной сортировкой левого и правого суб-массивов.
Если вы только хотите переместить n - й элемент на место, вместо рекурсии в и суб-массивы, вы можете сказать на каждом шаге, нужно ли вам спуститься в левый или правый суб-массив. (Ты знаешь это, потому что the n-го элемента в отсортированном массиве имеет индекс n таким образом, это становится вопросом сравнения индексов.) Таким образом, если ваш Quicksort не страдает наихудшим вырождением, вы примерно вдвое уменьшаете размер оставшегося массива на каждом шаге. (Вы больше никогда не смотрите на другой суб-массив.) Поэтому, в среднем, вы имеете дело с массивами следующих длин на каждом шаге:
- Θ(N)
- Θ(N / 2)
- Θ(N / 4)
- ...
каждый шаг линейного в длину массива это дело. (Вы перебираете его один раз и решаете, в какой суб-массив должен входить каждый элемент, в зависимости от того, как он сравнивается с pivot.)
вы можете видеть, что после Θ (log (N)) шаги, мы в конечном итоге достигнем одноэлементного массива и закончим. Если суммировать N (1 + 1/2 + 1/4 + ...), вы получите 2 N. Или, в среднем случае, поскольку мы не можем надеяться, что ось всегда будет точно медианой, что-то порядка Θ (N).
код 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), у вас есть сложность Линара в среднем.