Алгоритмы

индекс
296,98

Поиск k-ого наименьшего элемента

Сегодня на Хабре появилась очень интересная статья, о поиске минимального (максимального) значения на отрезке в массиве. Так как статья оказалось интересной и популярной, я решил с вами поделиться ещё одним алгоритмом поиска в массиве некоторых «специальных» значений.

Наверняка каждому встречалась задача нахождения k-ого наименьшего элемента в массиве. k-ый элемент характеризуется тем, что он больше (или равен) k элементов массива и меньше или равен N-k оставшихся элементов (где N – число элементов в массиве).

Задача нахождения k-ого наименьшего элемента обычно связывается с задачей сортировки, так как очевидный метод нахождения этого элемента состоит в сортировке N элементов и выборе k-ого.

Но мы с вами пойдём немного другим путём. Я предполагаю, что читатели знают, как работает алгоритм быстрой сортировки, но на всякий случай напомню. В массиве выбирается случайный элемент x, и выполнется просмотр массива слева, пока не найдётся элемент a[i]>x, затем выполняется просмотр справа, пока не будет найден элемент a[j]<x. Как только два таких элемента найдены, выполняется их обмен и просмотр продолжается до тех пор, пока индексы i,j не станут равны где-то в середине массива. В результате получается массив левая часть которого содержит элементы <=x, а правая часть содержит элементы >=x. Описанная процедура применяется рекурсивно для левой и правой части и продолжается до тех пор, пока не будет получен полностью отсортированный массив. (Немного подробнее о эффективных алгоритмах сортировки).

Процедура разделения, используемая в быстрой сортировке, даёт потенциальную возможность находить искомый (k-ый) элемент гораздо быстрее.
Этот алгоритм работает следующим образом. На первом шаге вызывается процедура разделения с L=1 и R=N (т.е. разделение выполняется для всего массива), причём в качестве разделяющего значения x выбирается a[k]. После разделения получаются значения индексов i,j такие, что

a[h]<x для всех h<i
a[h]>x для всех h>j
i>j

Здесь возможны три случая:
•Разделяющее значение x оказалось слишком мало. В результате граница между двумя частями меньше нужного значения k. Тогда операцию разделения нужно повторить с элементами a[i]…a[R].

•Выбранное значение x оказалось слишком велико. Тогда операцию разделения нужно повторить с элементами a[L]…a[j].

•Элемент a[k] разбивает массив на две части в нужной пропорции и поэтому является искомым значением.


Операцию разделения нужно повторять, пока не реализуется случай 3. Этот цикл выражается следующим фрагментом (прошу прощения за Pascal, но мои ученики пока знают только его):
  1. procedure Find(k: integer);
  2. var
  3. L,R,i,j: integer;
  4. w,x: integer;
  5. begin
  6.   L:=1; R:=N;
  7.   while L<R-1 do
  8.   begin
  9.     x:=a[k];
  10.     i:=L;
  11.     j:=R;
  12.     REPEAT
  13.       while a[i]<x do
  14.         i:=i+1;
  15.       while x<a[j] do
  16.         j:=j-1;
  17.       if i<=then
  18.       begin
  19.         w:=a[i];
  20.         a[i]:=a[j];
  21.         a[j]:=w;
  22.         i:=i+1;
  23.         j:=j-1;
  24.       end;
  25.     UNTIL i>j;
  26.     if j<k then
  27.       L:=i;
  28.     if k<i then
  29.       R:=j;
  30.   end;
  31. end;


Если предположить, что в среднем каждое разбиение делит пополам размер части массива, в которой находится искомое значение, то необходимое число сравнений будет N+N/2+N/4+…+1=2N. Это объясняет эффективность приведённой процедуры для поиска медиан и прочих величин, а также объясняет её превосходство над простым методом, состоящем в предварительной сортировке всего массива с последующим выбором k-ого элемента (где наилучшее поведение имеет порядок N*log(N)).

Надеюсь, этот алгоритм поможет вам сделать ваши программы более эффективными и быстрыми. Спасибо за внимание.
+38
6 марта 2011, 11:35
65

комментарии (26)

+4
Zlobober #
Стоит сказать, что в C++ этот алгоритм реализован в виде функции nth_element(A.begin(), A.begin() + n, A.end()), которая ставит на n-ое место, собственно, n-ое по величине число, пользуясь описанным алгоритмом.
+2
agul #
Достаточно подробно описано, спасибо.

Странно, что вы нигде в статье не использовали название алгоритма — «Порядковая статистика».
0
qrazydraqon #
k-я порядковая статистика — это то, что мы ищем, элемент с номером k в вариационном ряду. Вы уверены, что алгоритм называется так же?
+2
Laytlas #
Да, это называется «K-ая порядковая статистика».
+4
Megas #
прошу прощения за Pascal, но мои ученики пока знают только его

А причем тут ваши ученики?
0
GORKOFF #
Статья — фрагмент лекции.
+7
middle #
Обычно жалюутся, что «мои преподаватели знают только его» ;)
0
Shark #
Обычно жалуются только единицы, которые сами пытаются разобраться во всём. Остальные 80% группы знают только паскаль. А 10% из этих 80% и его не знают :) По крайней мере у той группы, в которой я проводил пары примерно так и было. Да и в моей так же было. Суровые реалии :)
0
middle #
Да, наверное, вы правы, 80% просто помалкивают в тряпочку, в результате создаётся ложное впечатление.
0
jolasveinninn #
k-ый элемент характеризуется тем, что он больше (или равен) k элементов массива...
k-ый элемент характеризуется тем, что он больше (или равен) k-1 элементов массива.
+1
smbd #
В массиве (2, 3, 1) второй элемент (2) больше или равен двум элементам — 1 и 2.
+1
jolasveinninn #
А в массиве (3, 2, 1) третий элемент меньше чем все остальные, т.е. ни разу не больше и не равен. Под к-ым элементом имеется ввиду к-ый по величине.
+1
smbd #
Я второй по величине и имел ввиду, посмотрите внимательно.
Третий по величине в (3, 2, 1) элемент (то бишь 3) больше или равен 3 элементам — 1, 2 и 3.
+1
jolasveinninn #
А разве сам k-ый элемент учитывается?
0
smbd #
Ну он же больше или равен самому себе, нет? :)
0
jolasveinninn #
А в массиве (3, 2, 1) третий элемент меньше чем все остальные, т.е. ни разу не больше и не равен. Под к-ым элементом имеется ввиду к-ый по величине.
+2
leventov #
Таким темпом через пару лет надо будет верстать «Алгоритмы. Построение и анализ (в хабрапереводе)».
0
Lazer1999 #
А такие алгоритмы называются к-оптимальными… Я по ним диплом писал :D
0
Lazer1999 #
И да, в любой задаче поиск k-го оптимального элемента имеет тот же класс сложности что и поиска 1го. Вопрос лишь в константе :)
0
Olostan #
Процедура разделения, используемая в быстрой сортировке, даёт потенциальную возможность находить искомый (k-ый) элемент гораздо быстрее.

гм, интересно… видно как кто изучал алгоритмы.

Я наоборот, думал что алгоритм нахождения k-ого меньшего элемента является базовым для квиксорта: «Quicksort is a sorting algorithm that splits the array in exactly the same way as the mean algorithm; and once the subarrays are sorted, by two recursive calls, there is nothing»

В статье в конце только чуть чуть упомянули оценку сложности алгоритма. На самом деле, если добавить сюда немного теории вероятности, то можно показать, что там идет спектр от O(n)...O(n^2), при чем вероятность O(n^2) очень и очень мала и зависит от размера массива (чем больше, тем меньше).

Но в целом статья в некоторой мере полезна своей минимальностью :)
0
GORKOFF #
Оценка сложности идёт по среднему случаю, а N^2 — наихудший.
0
Olostan #
В том то и задача — оценить «среднее». Это совсем не простая задача. В данной задаче выходит что при высоком n реализация алгоритма скорее стремится к нижнему порогу — O(N), что можно показать в принципе.

Даже более того, если поделить выпадание «хорошего» и «плохого» элементов (для нас «хороший» это если его значение в промежутке 1/4 до 3/4 от минимального до максимального) после двух операций деления, в худшем случае остаентся 3/4 массива (учитывая что для того, чтоб выпал хороший то надо в среднем 2 раза поделить). Что выходит

T(N) <= T(3*N/4)+O(N).

Что можно применить рекурсивно.

В итоге более справедливо будет сказать что средняя оценка будет не O(2N) как в статье, а O(N).
0
Achilles #
По-моему, Ваш код не будет работать, когда N=2 в силу условия цикла в строке №7 L<R-1.
0
bagyr #
Ладно паскаль, но однобуквенные имена переменных — это mauvais tone и антипаттерн как ни крути.
0
Mrrl #
Можно без особого труда заставить этот алгоритм гарантированно работать за линейное время. Если мы поделим массив на пятерки элементов, в каждой пятерке найдем средний элемент (6 сравнений на пятерку), а потом возьмем медиану X из этих средних элементов (рекурсия — R(N/5) операций, где R(N) — сложность исходного алгоритма), то нетрудно доказать, что порядковый номер этой медианы будет лежать между 3/10*N и 7/10*N: мы можем предъявить 3/10*N элементов массива, гарантированно не больших X, и столько же — гарантированно не меньших X. После еще X сравнений задача сводится к поиску какой-то статистики с массиве длиной не больше 7/10*N, т.е. R(N)<=R(N/5)+R(7*N/10)+11/5*N (считается число сравнений), т.е. R(N)<=22*N. Многовато, зато надежно. При больших N можно заменить 5 на большие числа (7,9,...), и искать в них медианы сначала оптимальным алгоритмом, а потом (при очень больших N) — этим же, рекурсивно. Правда, меняться будет только константа.
0
Mrrl #
* должно быть «порядковый номер этой медианы в вариационном ряду исходного массива»

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.