Как создать строго упорядоченные равномерно распределенные сегменты из массива?

Я хочу взять массив целых чисел и выполнить частичную сортировку этого массива. Каждый элемент в ведре перед ним меньше, чем текущие элементы ведра. Например, если у меня есть 10 ведер для значений 0-100, 0-9 пойдут в первое ведро, 10-19 во второе и так далее.

Например, я могу взять 1 12 23 44 48 и поместить их в 4 корзины из 10. Но если у меня есть 1, 2, 7, 4, 9, 1, то все значения попадают в одну корзину. Я ищу способ равномерно распределить значения по всем сегментам при сохранении порядка. Элементы в каждом сегменте не нужно сортировать. Например, я выгляжу похоже на это.

2 1 9 2 3 8 7 4 2 8 11 4 => [[2, 1], [2, 2], [3], [4], [4], [7], [8, 8], [9], [11]]

Я пытаюсь использовать это как быстрый способ разделить список на карте.

Спасибо за помощь.

Изменить, может быть, это проясняет ситуацию:

Я хочу создать хеш-функцию, в которой все элементы в ведре1 ‹ ведро2 ‹ ведро3..., где каждое ведро не отсортировано.


person user3509528    schedule 17.06.2017    source источник
comment
Должен ли он быть более эффективным, чем O(N.LogN)? В противном случае вы можете сначала отсортировать массив, а затем распределить его по корзинам. Кроме того, знаете ли вы, сколько целых чисел будет в массиве и каков их диапазон значений?   -  person m69 ''snarky and unwelcoming''    schedule 19.06.2017
comment
@ m69, должно быть намного быстрее. Я смотрю на 100 ТБ случайных данных, распространяющихся по всему спектру. Это часть операции карты, но не гарантируется равное разделение.   -  person user3509528    schedule 20.06.2017
comment
Я обновил свой ответ другим примером. Что-то из этого применимо к вашей проблеме?   -  person m69 ''snarky and unwelcoming''    schedule 20.06.2017
comment
Эй, m69, извините за задержку с ответами. Работа закипела, сегодня посмотрю. Спасибо еще раз.   -  person user3509528    schedule 27.06.2017
comment
Без проблем. :-)   -  person m69 ''snarky and unwelcoming''    schedule 28.06.2017


Ответы (1)


Если я правильно понимаю, у вас есть около 100 ТБ данных или 13 743 895 347 200 64-битных целых чисел без знака, которые вы хотите распределить по нескольким сегментам.

Первым шагом может быть повторение ввода, глядя, например. старшие 24 бита каждого целого числа и их подсчет. Это даст вам список из 16 777 216 диапазонов, каждый со средним числом 819 200, поэтому их можно хранить в 32-битных целых числах без знака, что займет 64 МБ.

Затем вы можете использовать это для создания таблицы поиска, которая сообщает вам, в какое ведро входит каждый из этих 16 777 216 диапазонов. Вы вычисляете, сколько целых чисел должно войти в каждое ведро (входной размер, разделенный на количество ведер), и проходите по массиву, сохраняя промежуточный итог подсчета, и устанавливаете каждый диапазон в ведро 1, пока промежуточный итог не станет слишком большим. для ведра 1, затем вы устанавливаете диапазоны для ведра 2 и так далее...

Конечно, всегда будет диапазон, который должен быть разделен между сегментом n и сегментом n+1. Чтобы отслеживать это, вы создаете вторую таблицу, в которой хранится, сколько целых чисел в этих разделенных диапазонах должно попасть в корзину n+1.

Итак, теперь у вас есть, например:

HIGH 24-BIT      RANGE           BUCKET    BUCKET+1

     0           0 ~    2^40-1     1          0
     1        2^40 ~  2*2^40-1     1          0
     2      2*2^40 ~  3*2^40-1     1          0
     3      3*2^40 ~  4*2^40-1     1          0
       ...
    16     16*2^40 ~ 17*2^40-1     1          0
    17     17*2^40 ~ 18*2^40-1     1    284,724  <- highest 284,724 go into bucket 2
    18     18*2^40 ~ 19*2^40-1     2          0
       ...

Теперь вы можете снова перебрать ввод и для каждого целого числа посмотреть на старшие 24 бита и использовать таблицу поиска, чтобы увидеть, в какое ведро должно попасть целое число. Если диапазон не разделен, вы можете сразу же переместить целое число в правую корзину. Для каждого диапазона разделения вы создаете упорядоченный список или очередь приоритетов, которая может содержать столько целых чисел, сколько необходимо для перехода в следующую корзину; вы сохраняете только самые высокие значения в этом списке или очереди; любое меньшее целое число отправляется прямо в корзину, а если целое число добавляется в полный список или очередь, наименьшее значение перемещается в корзину. В конце этот список или очередь добавляется к следующему сегменту.

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

Сложность этого составляет N для первого запуска, затем вы перебираете диапазоны R, затем N, когда вы снова перебираете ввод, а затем для разделенных диапазонов у вас будет что-то вроде M.logM для сортировки и M для распределения , так что в сумме 2*N + R + M.LogM + M. Использование большого количества диапазонов для уменьшения количества целых чисел в разделенных диапазонах, вероятно, будет лучшей стратегией для ускорения процесса.


На самом деле количество целых чисел M, которые находятся в разделенных диапазонах, зависит от количества сегментов B и диапазонов R, где M = N × B/R, так что, например, с тысячей сегментов и миллионом диапазонов 0,1% входных данных будут разделены на диапазоны и должны быть отсортированы. (Это средние значения, зависящие от фактического распределения.) Таким образом, общая сложность равна 2×N + R + (N×B/R).Log(N×B/R) + N×B /Р.

Другой пример:

Входные данные: N = 13 743 895 347 200 64-битных целых чисел без знака
Диапазоны: 232 (с использованием старших 32 битов каждого целого числа)
Целых чисел в диапазоне: 3200 (в среднем)
Список счетчиков: 232 16-битных целых чисел = 8 ГБ
Таблица поиска: 232 16-битных целых чисел = 8 ГБ
Разделить таблицу диапазонов : B 16-битных целых чисел = 2×B байтов

С 1024 сегментами это будет означать, что B/R = 1/222, и есть 1023 разделенных диапазона с примерно 3200 целыми числами в каждом, или всего около 3 276 800 целых чисел; затем их нужно будет отсортировать и распределить по ведрам.

С 1 048 576 сегментами это будет означать, что B/R = 1/212, и существует 1 048 575 разделенных диапазонов с примерно 3 200 целыми числами в каждом, или около 3 355 443 200 целых чисел в целом. (Конечно, для более чем 65 536 сегментов потребуется таблица поиска с 32-битными целыми числами.)

(Если вы обнаружите, что общее количество счетчиков в диапазоне не равно общему размеру входных данных, в списке счетчиков произошло переполнение, и вам следует переключиться на более крупный целочисленный тип для счетчиков.)< /я>


Давайте рассмотрим крошечный пример: 50 целых чисел в диапазоне от 1 до 100 должны быть распределены по 5 корзинам. Мы выбираем количество диапазонов, скажем, 20, и перебираем входные данные, чтобы подсчитать количество целых чисел в каждом диапазоне:

 2  9 14 17 21 30 33 36 44 50 51 57    69 75 80 81 87 94 99
 1  9 15 16 21    32 40 42 48 55 57    66 74 76    88    96
 5  6    20 24    34       50 52 58    70    78          99
    7                         51       69
                              55

 3  4  2  3  3  1  3  2  2  3  5  3  0  4  2  3  1  2  1  3  

Затем, зная, что каждое ведро должно содержать 10 целых чисел, мы перебираем список счетчиков для каждого диапазона и назначаем каждый диапазон ведру:

 3  4  2  3  3  1  3  2  2  3  5  3  0  4  2  3  1  2  1  3  <- count/range

 1  1  1  1  2  2  2  2  3  3  3  4  4  4  4  5  5  5  5  5  <- to bucket
          2           1        1                             <- to next

Когда диапазон необходимо разделить между двумя сегментами, мы сохраняем количество целых чисел, которые должны перейти к следующему сегменту, в отдельной таблице.

Затем мы можем снова перебрать ввод и переместить все целые числа в неразделенных диапазонах в корзины; целые числа в разделенных диапазонах временно перемещаются в отдельные сегменты:

bucket 1:  9 14  2  9  1 15  6  5  7
temp 1/2: 17 16 20
bucket 2: 21 33 30 32 21 24 34
temp 2/3: 36 40
bucket 3: 44 50 48 42 50
temp 3/4: 51 55 52 51 55
bucket 4: 57 75 69 66 74 57 57 70 69
bucket 5: 81 94 87 80 99 88 96 76 78 99

Затем мы смотрим на временные корзины одну за другой, находим x наибольших целых чисел, как указано во второй таблице, перемещаем их в следующую корзину и то, что осталось в предыдущей корзине:

temp 1/2: 17 16 20        (to next: 2)  bucket 1: 16           bucket 2: 17 20
temp 2/3: 36 40           (to next: 1)  bucket 2: 36           bucket 3: 40
temp 3/4: 51 55 52 51 55  (to next: 1)  bucket 3: 51 51 52 55  bucket 4: 55

И конечный результат:

bucket 1:  9 14  2  9  1 15  6  5  7 16
bucket 2: 21 33 30 32 21 24 34 17 20 36
bucket 3: 44 50 48 42 50 40 51 51 52 55
bucket 4: 57 75 69 66 74 57 57 70 69 55
bucket 5: 81 94 87 80 99 88 96 76 78 99

Итак, из 50 целых чисел нам пришлось отсортировать группу из 3, 2 и 5 целых чисел.


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

3  4  2  3  3  1  3  2  2  3  5  3  0  4  2  3  1  2  1  3  <- count/range
1  1  1  /  2  2  2  /  3  3  /  4  4  4  4  5  5  5  5  5  <- to bucket
bucket 1:  9 14  2  9  1 15  6  5  7     <- add 1
temp 1/2: 17 16 20                       <- 3-1 = 2 go to next bucket
bucket 2: 21 33 30 32 21 24 34           <- add 3-2 = 1
temp 2/3: 36 40                          <- 2-1 = 1 goes to next bucket
bucket 3: 44 50 48 42 50                 <- add 5-1 = 4
temp 3/4: 51 55 52 51 55                 <- 5-4 = 1 goes to next bucket
bucket 4: 57 75 69 66 74 57 57 70 69     <- add 1-1 = 0
bucket 5: 81 94 87 80 99 88 96 76 78 99  <- add 0

Вычисление того, какая часть входных данных будет находиться в разделенных диапазонах и должна быть отсортирована, приведенная выше как M = N × B/R, представляет собой среднее значение для примерно равномерно распределенных входных данных. Небольшое смещение с большим количеством значений в определенной части входного пространства не будет иметь большого эффекта, но действительно было бы возможно создать наихудший ввод, чтобы помешать алгоритму.

Давайте еще раз посмотрим на этот пример:

Входные данные: N = 13 743 895 347 200 64-битных целых чисел без знака
Диапазоны: 232 (с использованием старших 32 битов каждого целого числа)
Целых чисел в диапазоне: 3200 (в среднем)
Сегменты: 1 048 576
Целых чисел на сегмент: 13 107 200

Для начала, если есть диапазоны, содержащие более 232 целых чисел, вам придется использовать 64-битные целые числа для таблицы подсчета, поэтому ее размер будет 32 ГБ, что может заставить вас использовать меньше диапазонов, в зависимости от доступной памяти.

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

Если у вас достаточно памяти для выполнения первого шага с использованием 232 диапазонов, то каждый диапазон будет иметь 232 различных значений, и вы можете распределить разделенные диапазоны по сегментам, используя сортировка подсчетом (имеющая линейную сложность).

Если у вас недостаточно памяти для использования 232 диапазонов, и вы получаете проблемно большие разделенные диапазоны, вы можете снова использовать полный алгоритм для разделенных диапазонов. Допустим, вы использовали 228 диапазонов, ожидая, что каждый диапазон будет содержать около 51 200 целых чисел, и в итоге вы получили неожиданно большой разделенный диапазон с 5 120 000 000 целых чисел, которые необходимо распределить по 391 сегменту. Если вы снова запустите алгоритм для этого ограниченного диапазона, у вас будет 228 диапазонов (каждый из которых содержит в среднем 19 целых чисел с максимум 16 различными значениями) всего для 391 корзины и лишь небольшой риск снова оказаться с большими разделенными диапазонами.


Примечание. Диапазоны, которые должны быть разделены на два или более сегмента, не обязательно должны быть отсортированы. Вы можете, например. используйте рекурсивную версию алгоритма голландского национального флага Дейкстры, чтобы разделить диапазон на часть с наименьшими значениями x и часть с наибольшими значениями. Средняя сложность разбиения будет линейной (при использовании случайного поворота) по сравнению со сложностью сортировки O(N.LogN).

person m69 ''snarky and unwelcoming''    schedule 20.06.2017
comment
Вау, это феноменальный ответ. Мне было любопытно, как вы держите его строго возрастающим. кажется, что вам почти нужно отсортировать каждое ведро, чтобы найти максимальное значение для каждого ведра, чтобы перейти к следующему ведру. Что происходит, когда каждый отдельный элемент имеет одно и то же значение? Будет ли это по-прежнему эффективно сбалансировать его? - person user3509528; 27.06.2017
comment
@user3509528 user3509528 Дело в том, что вам нужно использовать больше диапазонов, чем сегментов; если у вас есть, например. В 100 раз больше диапазонов, чем сегментов, только 1% входных данных нуждается в сортировке. Но это правда, что это среднее значение для входных данных, которые примерно равномерно распределены, и можно было бы намеренно создать входные данные для наихудшего случая. Я расширю ответ, чтобы решить эту проблему. - person m69 ''snarky and unwelcoming''; 28.06.2017
comment
Кстати, сколько сегментов вы будете использовать, каковы ограничения памяти, и ожидаете ли вы много повторяющихся значений или любое другое известное смещение во входных данных? - person m69 ''snarky and unwelcoming''; 28.06.2017
comment
примерно 3000-20000р. Память может быть около 10 ТБ, разделенной между всеми узлами/сегментами. Мне нужно примерно 64 ГБ или меньше данных в каждом сегменте. Я могу сделать и то, и другое, но я хотел, чтобы он мог обрабатывать очень предвзятый ввод, если это возможно. - person user3509528; 29.06.2017