Какой самый эффективный (быстрый) способ найти N-е число наибольших целых чисел в массиве на C?

#c #performance

#c #Производительность

Вопрос:

Пусть у нас будет массив размером 8, Пусть N будет 3

С массивом: 1 3 2 17 19 23 0 2

Наш результат должен быть: 23, 19, 17

Объяснение: три самых больших числа из массива, перечисленные в порядке убывания.

Я пробовал это:

 int array[8];
int largest[N] = {0, 0, 0};

for (int i = 1; i < N; i  ) {
    for (int j = 0; j < SIZE_OF_ARRAY; j  ) {
        if (largest[i] > array[j]) {
            largest[i] = array[j];
            array[j] = 0;
        }
    }
}
  

Кроме того, пусть ограничение будет таким:

целые числа в массиве должны быть 0 <= i <= 1 000

N должно быть 1 <= N <= SIZE_OF_ARRAY — 1

РАЗМЕР_О_ПАРАГРАФА должен быть 2 <= РАЗМЕР_О_ПАРАГРАФА <= 1 000 000

Мой способ его реализации очень неэффективен, поскольку он очищает весь массив N раз. С огромными массивами это может занять несколько минут.

Какой был бы самый быстрый и эффективный способ реализовать это на C?

Комментарии:

1. Вы можете найти три самых больших значения за один проход, хотя в вопросе не указано, что делать с дубликатами.

2. @peepeepoopoo Неясно, учитываются ли дублированные максимальные числа.

3. Для массива размером 1000000 и N = 10000 самый быстрый метод — создать гистограмму элементов массива. Поскольку задано, что элементы массива находятся в диапазоне от 0 до 1000, размер гистограммы будет равен 1001. Время выполнения: O(array size) .

4. @VladfromMoscow Повторяющиеся целые числа также учитываются. Выходные данные могут содержать повторяющиеся целые числа или даже быть числом N из одного и того же целого числа, если это диктует входной массив

5. Алгоритм быстрого выбора разделит массив, чтобы найти N-е наибольшее (или наименьшее) значение; все значения после (до), которые будут больше (меньше). Это делается без выполнения полной сортировки в массиве, чего, по-видимому, достаточно для ваших целей. Я думаю, что это то, что предлагает @perreal, но я не уверен, почему «wiki it» является подходящим направлением.

Ответ №1:

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

 #define MAX_VALUE 1000
int occurrences[MAX_VALUE 1];
int largest[N];
int i, j;

for (i=0; i<N; i  )
    largest[N] = -1;

for (i=0; i<=MAX_VALUE; i  )
    occurrences[i] = 0;

for (i=0; i<SIZE_OF_ARRAY; i  )
    occurrences[array[i]]  ;

// Step through the occurrences array backward to find the N largest values.
for (i=MAX_VALUE, j=0, i; i>=0 amp;amp; j<N; i--)
    if (occurrences[i] > 0)
        largest[j  ] = i;
  

Обратите внимание, что это приведет только к одному элементу в largest для каждого уникального значения. Измените вставку соответствующим образом, если вы хотите, чтобы все вхождения отображались в largest . Из-за этого вы можете получить значения -1 для некоторых элементов, если не было достаточно уникальных больших чисел для заполнения largest массива. Наконец, результаты в largest будут отсортированы от наибольшего к наименьшему. Это будет легко исправить, если вы хотите: просто заполните largest массив справа налево.

Комментарии:

1. Похоже, что в этом случае будут отсутствовать дубликаты, хотя это можно исправить с помощью небольшой настройки.

2. Правильно. Я отметил это в своем ответе. Неясен вопрос о том, как обрабатывать дубликаты. И вы правы, что отслеживать кратные числа тривиально: количество — это значение каждого occurrences элемента массива.

3. Для обработки массива из N целых чисел, вероятно, вашему » occurrences[MAX_VALUE 1] » потребуется обрабатывать отрицательные числа, и, вероятно, ему потребуется 16 гигабайт памяти. Для больших массивов вы, вероятно, потратите так много времени на передачу данных в / из пространства подкачки, что пользователь умрет от старости до завершения вашего кода.

4. Диапазон, указанный в задаче, равен 0 ..1000.

Ответ №2:

Самый быстрый способ — распознать, что данные не появляются просто так (они либо существуют во время компиляции, либо поступают с помощью ввода-вывода — из файлов, из сети и т.д.); И поэтому вы можете найти 3 самых высоких значения при создании данных (во время компиляции; или когда вы анализируете и проверяете работоспособность, а затем сохраняете данные, полученные с помощью ввода-вывода — из файлов, из сети и т.д.). Вероятно, это самый быстрый из возможных способов (потому что вы либо ничего не делаете во время выполнения, либо избегаете необходимости просматривать все данные во второй раз).

Однако; в этом случае, если данные изменяются после их создания, вам нужно будет обновить «3 самых высоких значения» одновременно с изменением данных; что легко, если меньшее значение заменяется более высоким значением (вы просто проверяете, становится ли новое значение одним из 3 самых высоких значений), но включает поиск, если «ранее самое высокое» значение заменяется более низким значением.

Если вам нужно выполнить поиск; тогда это можно сделать с помощью одного цикла, например:

     firstHighest = INT_MIN;
    secondHighest = INT_MIN;
    thirdHighest = INT_MIN;

    for (int i = 1; i < N; i  ) {
        if(array[i] > thirdHighest) {
            if(array[i] > secondHighest) {
                if(array[i] > firstHighest) {
                    thirdHighest = secondHighest;
                    secondHighest = firstHighest;
                    firstHighest = array[i];
                } else {
                    thirdHighest = secondHighest;
                    secondHighest = array[i];
                }
            } else {
                thirdHighest = array[i];
            }
        }
    }
  

Примечание: Точный код будет зависеть от того, что вы хотите сделать с дубликатами (возможно, вам потребуется заменить if(array[j] > secondHighest) { на if(array[j] >= secondHighest) { и if(array[j] > firstHighest) { на if(array[j] >= firstHighest) { , если вам нужны числа 1, 2, 3, 4, 4, 4, 4 дать ответ 4, 4, 4 вместо 2, 3, 4).

Для больших объемов данных это может быть ускорено с помощью SIMD и / или нескольких потоков. Например; если SIMD может выполнять «связки из 8 целых чисел», и у вас есть 4 процессора (и 4 потока); затем вы можете разделить его на четверти, а затем обработать каждую четверть как столбцы из 8 элементов; найдите 3 самых высоких значения в каждом столбце в каждом квартале; затем определите 3 самых высоких значения из «3 самых высоких значений в каждом столбце в каждом квартале». В этом случае вы, вероятно, захотите добавить дополнение (фиктивные значения, установленные на INT_MIN ) в конец массива, чтобы гарантировать, что общий размер массива кратен ширине SIMD и количеству процессоров.

Для небольших объемов данных дополнительные накладные расходы на настройку SIMD и / или координацию нескольких потоков будут стоить больше, чем это экономит; и версия «простого цикла», вероятно, будет настолько быстрой, насколько это возможно.

Для неизвестных / переменных объемов данных вы могли бы предоставить несколько альтернатив (простой цикл, SIMD с одним потоком и SIMD с переменным количеством потоков) и решить, какой метод использовать (и сколько потоков использовать) во время выполнения, исходя из объема данных.

Комментарии:

1. Вау, этот ответ намного превосходит. Спасибо, что нашли время сообщить об этом, это было очень полезно!

2. Честно говоря, я удивлен, что этот ответ был принят. Это, конечно, не соответствует исходным требованиям. Ниже приведен превосходный ответ от @Howlium.

3. @VladFeinstein: Подход с гистограммой — это ужасное снижение производительности, потому что вы неоднократно обращаетесь / изменяете таблицу «вхождений» способами, которые не могут легко масштабироваться (для нескольких процессоров или SIMD), а также загружаете дневной свет из кэша «вероятно, наихудшими из возможных» шаблонами доступа, чтобы сгенерировать кучу информации, которая вам просто не нужна в первую очередь.

4. @Brendan — это может быть (не подходит для кэша), но имеет линейную сложность, в то время как ваш квадратичный (когда N близко к размеру этого массива).

5. @VladFeinstein: N является константой, а N равно 3. Если N близко к размеру массива, то массив содержит 3 элемента (настолько маленькие, что производительность не имеет значения). Мой подход — «O (M)» (где M — длина массива). Ваш подход также «O (M)», но ваш «O» на порядок хуже.

Ответ №3:

Один из методов, который я могу придумать, — это просто отсортировать массив и вернуть первые N чисел. Поскольку массив отсортирован, возвращаемое нами N число будет N наибольшими числами массива. Этот метод потребует временной сложности, O(nlogn) где n — количество элементов, которые у нас есть в данном массиве. Я думаю, что это, вероятно, очень хорошая временная сложность, которую вы можете получить при подходе к этой проблеме.

Другим подходом с аналогичной временной сложностью было бы использование max-heap . Сформируйте максимальную кучу из данного массива и N раз используйте pop() (или извлекайте, или как вы это называете), чтобы получить самый верхний элемент, который был бы максимальным элементом, оставшимся в куче после каждого pop .

Временную сложность этого подхода можно считать даже лучшей, чем у первого — O(n Nlogn) где n — количество элементов в массиве, а N — количество самых больших элементов, которые нужно найти. Здесь O(n) потребовалось бы собрать кучу, а для извлечения самого верхнего элемента нам понадобилось бы O(logn) N раз, что в сумме равно — O(n Nlogn) , немного лучше, чем O(nlogn)

Комментарии:

1. Не самый быстрый, поскольку метод гистограммы требует O (N)