Задача о m максимумах

от автора

Старая-старая школьно-студенческая задача… Дан массив целых чисел. Требуется найти m его максимальных (или минимальных) элементов. Когда я задаю эту задачу учащимся, то почти в каждой группе находятся «умельцы», решающие ее с помощью сортировки. В самом деле, это ведь так просто: сортируем массив по убыванию (вручную или подходящей библиотечной функцией) и просто берем m первых элементов. Такое решение всегда казалось мне алгоритмическим варварством — найти m максимумов достаточно просто за один проход массива; сортировка существенно сложнее. И однажды я решил исследовать сей вопрос вычислительными экспериментами. Результаты этих экспериментов я и предлагаю вашему вниманию. Не исключено, что кому-то результаты помогут и в практической работе. Намекну — интуиция меня в целом не подвела.

Spoiler

Без сортировки задача может быть решена, например, так. Создаем рабочий массив длины m и заполняем его начальными значениями. В общем случае можно в качестве такого значения выбрать минимальное значение int/integer для соответствующей среды программирования. А если известна нижняя граница значений исходного массива, то можно взять любое число, меньшее этой границы.

Итак рабочий массив заполнен одинаковыми значениями. Теперь берем элемент за элементом исходного массива и вставляем его в нужное место рабочего массива. При этом длину рабочего массива сохраняем равной m (после вставки последний элемент пропадает). Если очередной элемент меньше последнего значения рабочего массива, то он просто пропускается. Этот процесс имеет вычислительную сложность O(nm). Тогда как сортировка в лучшем случае описывается асимптотикой O(n*og(n)). Асимптотики показывают, как ведет себя функция (в данном случае — время сортировки) при стремлении параметров к бесконечности. Можно сказать, что время описанного алгоритма при стремлении n к бесконечности задается формулой t1=k1*O(n), а время сортировки t2=k2*O(n*log(n)). Здесь k1 и k2 — некие константы, зависящие от процессора, языка программирования, операционной системы и других факторов.

Я построил три системы тестов (для Питона, Java и VBA). Все тесты устроены по сути одинаково: строились массивы возрастающих размеров, заполнялись случайными числами задаваемого диапазона, сортировались с фиксацией времени и прогонялись через описанный выше алгоритм тоже с фиксацией времени. Каждый тест повторялся 10 раз и время усреднялось. В Питоне и Java использовалась встроенная сортировка, в VBA — реализация QuickSort.

Питон

Ниже показан код питоновских тестов.

import time from random import randint      def max_n_1(arr,n):     return sorted(arr,reverse=True)[0:n]      def max_n_2(arr,n):     res=[-1 for _ in range(n)]     for x in arr:         if x > res[n-1]:             i=n-1             j=i-1             while(j>=0 and res[j]<x):                 res[i]=res[j]                 i=i-1                 j=j-1             res[i]=x     return res        def start():     k=10     n=10000     print("k=",k)     while(n<=500000):         print("n=",n,end=' ')          t1=0.0         for i in range(10):             arr=[randint(1,2000) for _ in range(n)]             start_time = time.time()             z1=max_n_1(arr,k)             fin_time = time.time()             t1=t1+(fin_time-start_time)         print(t1/10.0,end=' ')              t2=0.0         for i in range(10):              arr=[randint(1,2000) for _ in range(n)]             start_time = time.time()             z2=max_n_2(arr,k)             fin_time = time.time()             t2=t2+(fin_time-start_time)         print(t2/10.0)              n+=10000          start() 

Размеры массива менялись от 10 до 500 тыс. элементов с шагом 10 тыс. Было выполнено два прогона: определение пяти и десяти максимумов. Результат для 10 максимумов показан ниже.

Время здесь приведено в миллисекундах. Что видим? Сортировка отстает (на краю интервала — вдвое). Для пяти максимумов картина аналогична. И надо заметить, что хотя питоновская сортировка очень эффективна, простой алгоритм оказывается быстрее. Заметны резкие провалы в производительности (зубцы на графиках). Они, вероятно, объясняются влиянием внутренних процессов (типа сборки мусора). Это же замечание относится и к другим графикам.

Java

Код тестов выглядел так:

import java.util.*;  class Start {    public static void main(String [] args)    {        Random random = new Random();     Scanner inp = new Scanner(System.in);     long startTime,endTime,tot1,tot2;     int i,a,b,n,m,x,ii,jj,u; 	     a=1;     b=3000; // диапазон случайных чисел [a,b]     m=10;          System.out.println(a+" "+b+" "+m);     for (n=50000; n<=5000000; n+=50000)     { 	    int arr[] = new int[n];       int ma[]  = new int[m]; 	       tot1=0;       for (u=0; u<10; u++)       {         	      for (i=0; i<n; i++) 	      { 		     arr[i]=a+random.nextInt(b-a+1); 	      }         startTime = System.currentTimeMillis();         Arrays.sort(arr);         endTime = System.currentTimeMillis();         tot1=tot1+(endTime-startTime);       }        tot2=0;       for (u=0; u<10; u++)       {         	      for (i=0; i<n; i++) 	      { 		      arr[i]=a+random.nextInt(b-a+1); 	      }             startTime = System.currentTimeMillis(); 	      for (i=0; i<m; i++) ma[i]=-999999; 	      for (i=0; i<n; i++) 	      {             x=arr[i];                     if (x >= ma[m-1])             {               ii=m-1;               jj=ii-1;               while(jj>=0 && ma[jj]<x)               {                 ma[ii]=ma[jj];                 ii--;                 jj--;               }                 ma[ii]=x;             }           }           endTime = System.currentTimeMillis();           tot2=tot2+(endTime-startTime);         }       System.out.println(n+" "+tot1+" "+tot2); 	   	}   }  }

Здесь размер массива тоже менялся от 10 тыс. до 500 тыс. элементов. Время — в миллисекундах. Результат оказался весьма похожим на питоновский (только сортировка Javа, увы, медленнее).

VBA

В этом языке нет универсальной встроенной сортировки (можно, правда, сортировать ячейки листа, но в этом случае будут велики накладные расходы, связанные с загрузкой и выгрузкой данных). Поэтому пришлось реализовать QuickSort вручную. Все это выглядит так:

Private Declare Function GetTickCount Lib "kernel32" () As Long  '::: Собственно сортировка Sub QSort(A() As Long, Optional b As Long = 1, Optional e As Long = 0)     If b > e Then Exit Sub     i& = b     j& = e     w& = A((i& + j&) / 2)     Do While (True)       Do While (A(i&) < w&)          i& = i& + 1       Loop       Do While (A(j&) > w&)          j& = j& - 1       Loop       If i& <= j& Then          Tmp& = A(i&)          A(i&) = A(j&)          A(j&) = Tmp&          i& = i& + 1          j& = j& - 1       End If       If i& > j& Then Exit Do     Loop          If j& > b Then QSort A, b, j&     If i& < e Then QSort A, i&, e  End Sub  '::: Проверка успешности сортировки  Function check(X() As Long) As Boolean      n& = UBound(X)      For i& = 1 To n& - 1          If X(i& + 1) < X(i&) Then             Debug.Print "Err! i=" + CStr(i&)             check = False             Exit Function          End If      Next i&      check = True End Function  '::: Вставка в упорядоченный массив Sub ins_in_arr(X As Long, A() As Long, n As Integer)     If X < A(n) Then Exit Sub     For i% = 1 To n         If X > A(i%) Then            For j% = n To i% + 1 Step -1                A(j%) = A(j% - 1)            Next j%            A(i%) = X            Exit Sub         End If     Next i% End Sub  '::: Собственно тест Sub test() Const sz = 500 Dim X() As Long Dim Ma(1 To sz) As Long     Randomize     ooo& = 1      For n& = 10000 To 500000 Step 10000                  t1# = 0         For nc% = 1 To 10                                  ReDim X(1 To n&) As Long             For i& = 1 To n&                 X(i&) = Rnd() * 5000             Next i&             s1& = GetTickCount             For i& = 1 To sz                 Ma(i&) = -2147483647             Next i&             For i& = 1 To n&                 ins_in_arr X(i&), Ma, 10             Next i&             s2& = GetTickCount             t1# = t1# + s2& - s1&                  Next nc%                          Cells(ooo&, 1).Value = n&         Cells(ooo&, 2).Value = t1# / 10                          t2# = 0                  For nc% = 1 To 10                      ReDim X(1 To n&) As Long             For i& = 1 To n&                 X(i&) = Rnd() * 5000             Next i&             s1& = GetTickCount             QSort X, 1, n&             s2& = GetTickCount             If Not check(X) Then                MsgBox "Ошибка при сортировке!"                Exit Sub             End If             t2# = t2# + s2& - s1&          Next nc%          Cells(ooo&, 3).Value = t2# / 10         ooo& = ooo& + 1              Next n&  End Sub

На каждом витке цикла корректность сортировки проверяется. Время проверки, естественно, не включается в общий хронометраж. Набор исходных данных тот же — от 10 до 500 тыс. целых чисел. Получает, в общем, похожая картина:

Представляет некоторый интерес изучить зависимость времени от количества искомых максимумов (при фиксированном размере массива). Здесь, очевидно, сортировка будет тем выгоднее, чем больше максимумов требуется найти. А вставка в упорядоченный массив будет тем медленнее, чем массив больше.

Самым невыгодным случаем будет являться, как ни странно, входной массив, уже упорядоченный по возрастанию. В этом случае количество сравнений при вставке достигнет максимума и будет равно n*m. Массив, упорядоченный по убыванию, напротив, весьма выгоден. Число сравнений здесь будет ~ m+n.

Описанный в самом начале статьи алгоритм, можно ускорить, если вместо простого упорядоченного массива использовать древовидную или пирамидальную структуру. Именно так и реализована в Питоне в модуле heapq функция nsmallest.

Для небольших массивов (несколько тысяч элементов и менее) разница в подходах представляется несущественной. И если нужно быстро написать код, то сортировка — вполне приемлемое решение. Для больших массивов выгоднее от сортировки отказаться.

Вот и все, что я хотел рассказать об этой задаче. Спасибо, что дочитали до конца. С наступившим 2021-м годом!

ссылка на оригинал статьи https://habr.com/ru/post/535870/