Найти целое число не среди четырех миллиардов заданных - PullRequest
675 голосов
/ 23 августа 2011

Это вопрос интервью:

Учитывая входной файл с четырьмя миллиардами целых чисел, предоставьте алгоритм для генерации целого числа, которого нет в файле.Предположим, у вас есть 1 ГБ памяти.Следуйте тому, что вы сделали бы, если бы у вас было только 10 МБ памяти.

Мой анализ:

Размер файла 4 × 10 9 × 4 байта = 16 ГБ.

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

Мое понимание (после прочтения всех ответов):

Предполагается, что речь идет о 32-разрядных целых числах.,Есть 2 ^ 32 = 4 * 10 9 различных целых чисел.

Случай 1: у нас есть 1 ГБ = 1 * 10 9 * 8 бит = 8 миллиардов битпамять.

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

int radix = 8;
byte[] bitfield = new byte[0xffffffff/radix];
void F() throws FileNotFoundException{
    Scanner in = new Scanner(new FileReader("a.txt"));
    while(in.hasNextInt()){
        int n = in.nextInt();
        bitfield[n/radix] |= (1 << (n%radix));
    }

    for(int i = 0; i< bitfield.lenght; i++){
        for(int j =0; j<radix; j++){
            if( (bitfield[i] & (1<<j)) == 0) System.out.print(i*radix+j);
        }
    }
}

Случай 2: 10 МБ памяти = 10 * 10 6 * 8 бит = 80 миллионов бит

Решение: Для всех возможных 16-битные префиксы, есть 2 ^ 16, число целых = 65536, нам нужно 2 ^ 16 * 4 * 8 = 2 миллиона бит.Нам нужно построить 65536 ведер.Для каждого сегмента нам нужно 4 байта, в которых хранятся все возможности, поскольку в худшем случае все 4 миллиарда целых чисел принадлежат одному и тому же блоку.

  1. Построить счетчик каждого блока через первый проход по файлу.
  2. Просканируйте сегменты, найдите первого, у которого было меньше 65536 попаданий.
  3. Создайте новые сегменты с высокими 16-битными префиксами, которые мы нашли на шаге 2 через второй проход файла
  4. Сканирование сегментов, созданных в шаге 3, найдите первое, которое не имеет попадания.

Код очень похож на приведенный выше.

Вывод: Мыуменьшить объем памяти за счет увеличения пропускной способности файла.


Уточнение для тех, кто опаздывает: в вопросе не говорится, что в файле нет ровно одного целого числа -по крайней мере, это не так, как большинство людей интерпретируют это.Тем не менее, многие комментарии в ветке комментариев касаются этого варианта задачи.К сожалению, комментарий о том, что представил его в ветке комментариев, был позже удален его автором, так что теперь он выглядит так, как будто осиротевшие ответы на него просто неправильно поняли все.Это очень запутанно.К сожалению.

Ответы [ 38 ]

521 голосов
/ 23 августа 2011

Предполагая, что "целое число" означает 32 бита : наличия 10 МБ свободного места более чем достаточно для подсчета количества чисел во входном файле с любым данным 16-разрядным префиксом для всех возможные 16-битные префиксы за один проход через входной файл. По крайней мере, одно из ведер будет поражено менее 2 ^ 16 раз. Сделайте второй проход, чтобы узнать, какие из возможных чисел в этом сегменте уже используются.

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

Если "целое число" означает математическое целое число : прочитайте один раз ввод и отследите длину наибольшее число длину самого длинного числа, которое вы когда-либо видели. Когда вы закончите, выведите максимум плюс один случайное число, которое имеет еще одну цифру. (Одно из чисел в файле может быть bignum, для точного представления которого требуется более 10 МБ, но если вход представляет собой файл, то вы можете по крайней мере представить длину всего, что в него вписывается ).

194 голосов
/ 23 августа 2011

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

Если допускаются очень большие целые числа , то можно сгенерировать число, которое, вероятно, будет уникальным в O (1ВремяПсевдослучайное 128-разрядное целое число, такое как GUID , будет конфликтовать только с одним из четырех миллиардов целых чисел в наборе менее чем в одном из каждых 64 миллиардов миллиардов случаев.

Если целые числа ограничены 32 битами , то можно сгенерировать число, которое, вероятно, будет уникальным за один проход, используя намного меньше, чем 10 МБ.Вероятность того, что псевдослучайное 32-разрядное целое число столкнется с одним из 4 миллиардов существующих целых чисел, составляет около 93% (4e9 / 2 ^ 32).Вероятность того, что все 1000 псевдослучайных целых будут сталкиваться, составляет менее одного на 12 000 миллиардов миллиардов миллиардов (вероятность одного столкновения - 1000).Поэтому, если программа поддерживает структуру данных, содержащую 1000 псевдослучайных кандидатов, и перебирает известные целые числа, исключая совпадения из кандидатов, то почти наверняка найдется хотя бы одно целое число, которого нет в файле.

141 голосов
/ 23 августа 2011

Подробное обсуждение этой проблемы обсуждалось в Джон Бентли"Колонка 1. Трещина в устрице" Программирование жемчужин Аддисон-Уэсли стр.3-10

Бентли обсуждает несколько подходов, включая внешнюю сортировку, сортировку слиянием с использованием нескольких внешних файлов и т. Д. Но лучший метод, который предлагает Бентли, - это однопроходный алгоритм с использованием битовых полей , который он с юмором называет"Wonder Sort" :) В связи с проблемой, 4 миллиарда чисел могут быть представлены в:

4 billion bits = (4000000000 / 8) bytes = about 0.466 GB

Код для реализации набора битов прост: (взят из страница решений )

#define BITSPERWORD 32
#define SHIFT 5
#define MASK 0x1F
#define N 10000000
int a[1 + N/BITSPERWORD];

void set(int i) {        a[i>>SHIFT] |=  (1<<(i & MASK)); }
void clr(int i) {        a[i>>SHIFT] &= ~(1<<(i & MASK)); }
int  test(int i){ return a[i>>SHIFT] &   (1<<(i & MASK)); }

Алгоритм Бентли делает один проход по файлу, set устанавливает соответствующий бит в массиве, а затем проверяет этот массив, используя макрос test, описанный выше, чтобы найти пропущенное число.

Если доступная память меньше 0,466 ГБ, Bentley предлагает алгоритм k-pass, который делит входные данные на диапазоны в зависимости от доступной памяти.Возьмем очень простой пример: если был доступен только 1 байт (то есть память для обработки 8 чисел), а диапазон был от 0 до 31, мы делим это на диапазоны от 0 до 7, 8-15, 16-22 и т. Д.и обрабатывать этот диапазон в каждом из 32/8 = 4 проходов.

HTH.

117 голосов
/ 23 августа 2011

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

56 голосов
/ 23 августа 2011

Для варианта RAM объемом 1 ГБ вы можете использовать битовый вектор. Вам нужно выделить 4 миллиарда битов == 500 МБ байтового массива. Для каждого числа, считываемого с входа, установите соответствующий бит на «1». Как только вы закончите, итерируйте по битам, найдите первый, который все еще равен «0». Его индекс является ответом.

45 голосов
/ 23 августа 2011

Если это 32-разрядные целые числа (вероятно, из выбора ~ 4 миллиардов чисел, близких к 2 32 ), ваш список из 4 миллиардов чисел займет не более 93% возможных целых чисел (4 * 10 9 / (2 32 )).Таким образом, если вы создаете битовый массив из 2 32 бит с каждым битом, инициализированным в ноль (который займет 2 29 байт ~ 500 МБ ОЗУ; запомните, что байт = 2 3 бит = 8 бит), прочитайте список целых чисел и для каждого целого установите соответствующий элемент массива битов от 0 до 1;а затем прочитайте ваш битовый массив и верните первый бит, который по-прежнему равен 0.

В случае, когда у вас меньше ОЗУ (~ 10 МБ), это решение необходимо слегка изменить.10 МБ ~ 83886080 битов все еще достаточно для создания битового массива для всех чисел от 0 до 83886079. Таким образом, вы можете прочитать свой список целых чисел;и только записи #, которые находятся между 0 и 83886079 в вашем битовом массиве.Если числа распределены случайным образом;с подавляющей вероятностью (она отличается на 100% примерно на 10 -2592069 ) вы найдете пропущенный int).Фактически, если вы выбираете только числа от 1 до 2048 (только с 256 байтами оперативной памяти), вы все равно найдете пропущенное число в подавляющем проценте (99.99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999995%) того времени.

Но, скажем, вместооколо 4 миллиардов номеров;у вас было что-то вроде 2 32 - 1 цифра и менее 10 МБ ОЗУ;поэтому любой небольшой диапазон целых имеет небольшую вероятность не содержать числа.

Если вам было гарантировано, что каждое целое в списке уникально, вы можете сложить числа и вычесть сумму с одним пропущенным # до полной суммы (½) (2 32 ) (2 32 - 1) = 9223372034707292160, чтобы найти отсутствующий int.Однако, если int встречался дважды, этот метод потерпит неудачу.

Однако вы всегда можете разделить и победить.Наивным способом было бы прочитать массив и подсчитать количество чисел, которые находятся в первой половине (от 0 до 2 31 -1) и во второй половине (2 31 ,2 * 32 * тысяча тридцать восемь * +1039 *).Затем выберите диапазон с меньшим числом чисел и повторите деление этого диапазона пополам.(Скажем, если в (2 31 , 2 32 ) чисел на два меньше, то при следующем поиске будет подсчитано число в диапазоне (2 31 , 3* 2 30 -1), (3 * 2 30 , 2 32 ). Повторяйте до тех пор, пока не найдете диапазон с нулевыми числами и получите ответ. Должно пройти O (LG N) ~ 32 чтения через массив.

Этот метод был неэффективен. Мы используем только два целых числа на каждом шаге (или около 8 байтов ОЗУ с 4 байтами (32-бит)) целое число). Лучшим методом было бы разделить на sqrt (2 32 ) = 2 16 = 65536 бинов, каждый с 65536 числами в бине. Каждый бин требует 4 байта длясохранить его счетчик, так что вам нужно 2 18 байт = 256 кБ. Таким образом, интервал 0 равен (от 0 до 65535 = 2 16 -1), интервал 1 равен (2 16) = от 65536 до 2 * 2 16 -1 = 131071), корзина 2 - (2 * 2 16 = от 131072 до 3 * 2 16 -1 = 196607). В python у вас будет что-то вроде:

import numpy as np
nums_in_bin = np.zeros(65536, dtype=np.uint32)
for N in four_billion_int_array:
    nums_in_bin[N // 65536] += 1
for bin_num, bin_count in enumerate(nums_in_bin):
    if bin_count < 65536:
        break # we have found an incomplete bin with missing ints (bin_num)

Прочитайте список целых чисел ~ 4 миллиарда и посчитайте, сколько целыхпопадайте в каждую из 2 16 корзин и найдите incomplete_bin, в котором нет всех 65536 чисел.Затем вы снова читаете список из 4 миллиардов целых чисел;но на этот раз заметьте только когда целые числа находятся в этом диапазоне;слегка переворачивая, когда вы найдете их.

del nums_in_bin # allow gc to free old 256kB array
from bitarray import bitarray
my_bit_array = bitarray(65536) # 32 kB
my_bit_array.setall(0)
for N in four_billion_int_array:
    if N // 65536 == bin_num:
        my_bit_array[N % 65536] = 1
for i, bit in enumerate(my_bit_array):
    if not bit:
        print bin_num*65536 + i
        break
37 голосов
/ 23 августа 2011

Зачем делать это так сложно?Вы запрашиваете целое число, отсутствующее в файле?

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

Нет риска ударить по максинту или чему-либо еще, потому что в соответствии с правилами нет ограничений на размер целого числа иличисло, возвращаемое алгоритмом.

32 голосов
/ 24 августа 2011

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

  1. Начните с допустимого диапазона чисел, от 0 до 4294967295.

  2. Рассчитать среднюю точку.

  3. Перебрать файл, подсчитав, сколько чисел было равно, меньше или больше значения средней точки.

  4. Если никакие числа не были равны, все готово.Число средней точки является ответом.

  5. В противном случае выберите диапазон с наименьшим числом и повторите шаг 2 с этим новым диапазоном.

Для этого потребуется до 32 линейных сканирований по файлу, но для хранения диапазона и количества будет использоваться только несколько байтов памяти.

По сути, это то же самое, что Решение Хеннинга , за исключением того, что вместо 16 КБ используются две корзины.

27 голосов
/ 23 августа 2011

РЕДАКТИРОВАТЬ Хорошо, это не совсем продумано, поскольку предполагается, что целые числа в файле следуют некоторому статическому распределению. По-видимому, им не нужно, но даже тогда нужно попробовать это:


≈ 4,3 миллиарда 32-разрядных целых чисел. Мы не знаем, как они распределяются в файле, но наихудший случай - это случай с самой высокой энтропией Шеннона: равное распределение. В этом случае вероятность того, что какое-либо одно целое число не появится в файле, составляет

((2³²-1) / 2³²) ⁴ ⁰⁰⁰ ⁰⁰⁰ ⁰⁰⁰ ≈ .4

Чем ниже энтропия Шеннона, тем выше эта вероятность в среднем, но даже для этого наихудшего случая у нас есть шанс на 90% найти непериодическое число после 5 догадок со случайными целыми числами. Просто создайте такие числа с помощью псевдослучайного генератора, сохраните их в списке. Затем прочитайте int за int и сравните его со всеми вашими догадками. Когда есть совпадение, удалите эту запись списка. После того, как вы просмотрели весь файл, скорее всего, у вас останется более одного предположения. Используйте любой из них. В редком (10%, даже в худшем случае) событии без догадок остаётся получить новый набор случайных целых чисел, возможно, больше на этот раз (10-> 99%).

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


Реальный худший случай, когда мы не предполагаем статическое распределение, это то, что каждое целое число встречается макс. один раз, потому что только тогда 1 - 4000000000 / 2³² ≈ 6% из всех целых чисел не встречаются в файле. Так что вам понадобится еще несколько догадок, но это все равно не потребует вредных объемов памяти.
24 голосов
/ 24 августа 2011

Если в диапазоне [0, 2 ^ x - 1] отсутствует одно целое число, просто скопируйте их все вместе.Например:

>>> 0 ^ 1 ^ 3
2
>>> 0 ^ 1 ^ 2 ^ 3 ^ 4 ^ 6 ^ 7
5

(я знаю, что это не отвечает на вопрос точно , но это хороший ответ на очень похожий вопрос.)

...