Интересный вопрос из интервью, который использует мой коллега:
Предположим, вам дан очень длинный несортированный список 64-битных целых чисел без знака. Как найти наименьшее неотрицательное целое число, которого нет в списке?
ПОСЛЕДУЮЩИЕ ДЕЙСТВИЯ: Теперь, когда было предложено очевидное решение путем сортировки, можете ли вы сделать это быстрее, чем O (n log n)?
ПОСЛЕДУЮЩИЕ ДЕЙСТВИЯ: ваш алгоритм должен работать на компьютере, скажем, с 1 ГБ памяти.
УТОЧНЕНИЕ: список находится в ОЗУ, хотя может занимать его большой объем. Вам заранее сообщается размер списка, скажем N.
Ответы:
Если структура данных может быть изменена на месте и поддерживает произвольный доступ, вы можете сделать это за O (N) времени и O (1) дополнительного пространства. Просто пройдите по массиву последовательно и для каждого индекса запишите значение индекса в индекс, указанный в value, рекурсивно помещая любое значение в этом месте на его место и отбрасывая значения> N. Затем снова пройдите по массиву в поисках места где значение не соответствует индексу - это наименьшее значение, отсутствующее в массиве. Это приводит к максимуму 3N сравнений и использует только несколько значений временного пространства.
источник
Вот простое
O(N)
решение, использующееO(N)
пространство. Я предполагаю, что мы ограничиваем входной список неотрицательными числами и хотим найти первое неотрицательное число, которого нет в списке.N
.N
логических значений, инициализированный для всехfalse
.X
в списке, еслиX
меньше, чемN
, установитеX'th
элемент массива равнымtrue
.0
, в поисках первого найденного элементаfalse
. Если вы найдете первоеfalse
по индексуI
, тоI
это ответ. В противном случае (т.е. когда все элементы естьtrue
) ответ будетN
.На практике «массив
N
логических значений», вероятно, будет закодирован как «битовая карта» или «битовый набор », представленный как массивbyte
илиint
. Обычно это занимает меньше места (в зависимости от языка программирования) и позволяетfalse
быстрее выполнить сканирование для первого .Вот как / почему работает алгоритм.
Предположим, что
N
числа в списке неотличимы, или что одно или несколько из них больше, чемN
. Это означает, что в диапазоне должен быть хотя бы один номер0 .. N - 1
, которого нет в списке. Таким образом, проблема поиска наименьшего пропущенного числа должна сводиться к проблеме поиска наименьшего пропущенного числа, меньшего, чемN
. Это означает, что нам не нужно отслеживать числа, которые больше или равныN
... потому что они не будут ответом.Альтернативой предыдущему абзацу является то, что список представляет собой перестановку чисел из
0 .. N - 1
. В этом случае шаг 3 устанавливает для всех элементов массива значениеtrue
, а шаг 4 сообщает нам, что первое «отсутствующее» число равноN
.Вычислительная сложность алгоритма
O(N)
при относительно небольшой константе пропорциональности. Он выполняет два линейных прохода по списку или только один проход, если известно, что длина списка начинается с. Нет необходимости представлять весь список в памяти, поэтому асимптотическое использование памяти алгоритмом - это как раз то, что необходимо для представления массива логических значений; т.е.O(N)
биты.(Напротив, алгоритмы, основанные на сортировке или разделении в памяти, предполагают, что вы можете представить весь список в памяти. В той форме, в которой был задан вопрос, для этого потребовались бы
O(N)
64-битные слова.)@Jorn отмечает, что шаги с 1 по 3 являются разновидностью сортировки с подсчетом. В некотором смысле он прав, но различия существенны:
Xmax - Xmin
счетчиков, гдеXmax
- наибольшее число в списке иXmin
наименьшее число в списке. Каждый счетчик должен представлять N состояний; т.е. предполагая двоичное представление, он должен иметь целочисленный тип (как минимум)ceiling(log2(N))
биты .Xmax
иXmin
.ceiling(log2(N)) * (Xmax - Xmin)
биты.Напротив, представленный выше алгоритм просто требует
N
битов в худшем и лучшем случаях.Однако этот анализ приводит к интуиции, что если бы алгоритм произвел первоначальный проход по списку в поисках нуля (и подсчитал элементы списка, если необходимо), он дал бы более быстрый ответ, не используя пробела вообще, если бы он нашел ноль. Это однозначно стоит сделать, если велика вероятность найти хотя бы один ноль в списке. И этот дополнительный проход не меняет общей сложности.
РЕДАКТИРОВАТЬ: я изменил описание алгоритма, чтобы использовать «массив логических значений», поскольку люди, по-видимому, сочли мое исходное описание с использованием битов и растровых изображений запутанным.
источник
bool[]
помощью растрового изображения, не имеет отношения к общему решению.Поскольку OP теперь указал, что исходный список хранится в ОЗУ и что компьютер имеет только, скажем, 1 ГБ памяти, я собираюсь рискнуть и предсказать, что ответ нулевой.
1 ГБ ОЗУ означает, что список может содержать не более 134 217 728 номеров. Но есть 2 64 = 18 446 744 073 709 551 616 возможных чисел. Таким образом, вероятность того, что ноль находится в списке, равна 1 из 137 438 953 472.
Для сравнения, мои шансы на то, что меня ударит молния в этом году, - 1 к 700 000. И мои шансы на попадание метеорита составляют примерно 1 из 10 триллионов. Так что вероятность того, что меня напишут в научном журнале из-за моей безвременной смерти от небесного объекта, примерно в десять раз выше, чем ответ, отличный от нуля.
источник
Как указано в других ответах, вы можете выполнить сортировку, а затем просто сканировать, пока не найдете пробел.
Вы можете улучшить алгоритмическую сложность до O (N) и сохранить пространство O (N), используя модифицированную QuickSort, в которой вы удаляете разделы, которые не являются потенциальными кандидатами на содержание пробела.
Это экономит большое количество вычислений.
источник
Чтобы проиллюстрировать одну из ловушек
O(N)
мышления, вотO(N)
алгоритм, использующийO(1)
пространство.источник
Поскольку все числа имеют длину 64 бита, мы можем использовать сортировку по основанию для них по , которая равна O (n). Сортируйте их, затем просматривайте, пока не найдете то, что ищете.
если наименьшее число равно нулю, бегите вперед, пока не найдете пробел. Если наименьшее число не равно нулю, ответ равен нулю.
источник
Для экономичного в пространстве метода, и все значения различны, вы можете делать это в пространстве
O( k )
и времениO( k*log(N)*N )
. Он занимает мало места, нет перемещения данных, и все операции элементарны (добавление и вычитание).U = N; L=0
k
регионы. Как это:0->(1/k)*(U-L) + L
,0->(2/k)*(U-L) + L
,0->(3/k)*(U-L) + L
...0->(U-L) + L
count{i}
) есть в каждом регионе. (N*k
шаги)h
неполный регион ( ). Это значитcount{h} < upper_limit{h}
. (k
шаги)h - count{h-1} = 1
у тебя есть ответU = count{h}; L = count{h-1}
это можно улучшить с помощью хеширования (спасибо Нику за эту идею).
k
регионы. Как это:L + (i/k)->L + (i+1/k)*(U-L)
inc count{j}
с помощьюj = (number - L)/k
(if L < number < U)
h
), в которой нет k элементовcount{h} = 1
ч твой ответU = maximum value in region h
L = minimum value in region h
Это будет работать
O(log(N)*N)
.источник
U-L < k
Я просто сортирую их, а затем просматриваю последовательность, пока не найду пробел (включая пробел в начале между нулем и первым числом).
С точки зрения алгоритма, это можно сделать примерно так:
Конечно, если у вас намного больше памяти, чем у процессора, вы можете создать битовую маску всех возможных 64-битных значений и просто установить биты для каждого числа в списке. Затем найдите первый 0-бит в этой битовой маске. Это превращает его в операцию O (n) с точки зрения времени, но чертовски дорого с точки зрения требований к памяти :-)
Я сомневаюсь, что вы могли бы улучшить O (n), так как я не вижу способа сделать это, чтобы не смотреть на каждое число хотя бы один раз.
Алгоритм для этого будет примерно таким:
источник
Отсортируйте список, посмотрите на первый и второй элементы и начните подниматься вверх, пока не появится пробел.
источник
Вы можете сделать это за O (n) раз и O (1) дополнительного места, хотя скрытый фактор довольно велик. Это непрактичный способ решения проблемы, но, тем не менее, он может быть интересным.
Для каждого 64-битного целого числа без знака (в порядке возрастания) перебирайте список, пока не найдете целевое целое число или не дойдете до конца списка. Если вы дойдете до конца списка, целевое целое число будет наименьшим целым числом, отсутствующим в списке. Если вы дойдете до конца 64-битных целых чисел, каждое 64-битное целое число будет в списке.
Вот это как функция Python:
Эта функция намеренно неэффективна, чтобы поддерживать ее O (n). Обратите особое внимание на то, что функция продолжает проверять целевые числа даже после того, как ответ был найден. Если функция вернется, как только ответ будет найден, количество запусков внешнего цикла будет ограничено размером ответа, который ограничен значением n. Это изменение сделает время выполнения O (n ^ 2), хотя это будет намного быстрее.
источник
Спасибо egon, swilden и Stephen C за вдохновение. Во-первых, мы знаем границы ценности цели, потому что она не может быть больше размера списка. Кроме того, список размером 1 ГБ может содержать не более 134217728 (128 * 2 ^ 20) 64-битных целых чисел.
Часть хеширования
Я предлагаю использовать хеширование, чтобы значительно сократить пространство поиска. Сначала извлеките квадратный корень из размера списка. Для списка размером 1 ГБ это N = 11 586. Создайте целочисленный массив размера N. Просмотрите список и возьмите квадратный корень * из каждого найденного числа в качестве хэша. В вашей хеш-таблице увеличьте счетчик для этого хэша. Затем переберите свою хеш-таблицу. Первое обнаруженное вами ведро, не равное максимальному размеру, определяет ваше новое пространство поиска.
Часть растрового изображения
Теперь настройте обычную битовую карту, равную размеру вашего нового пространства поиска, и снова выполните итерацию по исходному списку, заполняя растровое изображение по мере нахождения каждого числа в вашем пространстве поиска. Когда вы закончите, первый неустановленный бит в вашем растровом изображении даст вам ваш ответ.
Это будет выполнено за время O (n) и пространство O (sqrt (n)).
(* Вы можете использовать что-то вроде битового сдвига, чтобы сделать это намного более эффективно, и просто измените количество и размер сегментов соответственно.)
источник
Хорошо, если в списке чисел отсутствует только одно число, самый простой способ найти недостающее число - это просуммировать ряд и вычесть каждое значение в списке. Конечное значение - это пропущенное число.
источник
источник
Мы могли бы использовать хеш-таблицу для хранения чисел. Когда все числа будут выполнены, запустите счетчик от 0 до наименьшего. Достаточно хороший хеш будет хешировать и хранить в постоянное время и извлекать в постоянное время.
Худший случай, если
n
в массиве есть элементы, и{0, 1, ... n-1}
в этом случае ответ будет получен приn
сохранении егоO(n)
.источник
Вот мой ответ, написанный на Java:
Основная идея: 1. Прокрутите массив, выбрасывая повторяющиеся положительные, нули и отрицательные числа, суммируя остальные, получая максимальное положительное число и сохраняя уникальные положительные числа на карте.
2- Вычислите сумму как max * (max + 1) / 2.
3- Найдите разницу между суммами, рассчитанными на шагах 1 и 2.
4- Выполните цикл снова от 1 до минимума [сумма разницы, макс] и верните первое число, которого нет на карте, заполненной на шаге 1.
источник
Как разумно заметил Стивен С., ответ должен быть числом меньше длины массива. Тогда я бы нашел ответ с помощью бинарного поиска. Это оптимизирует наихудший случай (чтобы интервьюер не мог поймать вас в патологическом сценарии «что, если»). На собеседовании обязательно укажите, что вы делаете это для оптимизации в худшем случае.
Способ использования двоичного поиска - вычесть искомое число из каждого элемента массива и проверить наличие отрицательных результатов.
источник
Мне нравится метод «нулевого предположения». Если бы числа были случайными, очень вероятно, что ноль. Если «экзаменатор» установил неслучайный список, добавьте его и снова угадайте:
В худшем случае n * N с n = N, но на практике n, скорее всего, будет небольшим числом (например, 1).
источник
Я не уверен, понял ли я вопрос. Но если для списка 1,2,3,5,6 и недостающее число равно 4, то недостающее число можно найти в O (n) следующим образом: (n + 2) (n + 1) / 2- (n + 1) п / 2
РЕДАКТИРОВАТЬ: извините, я думаю, что вчера вечером я слишком быстро думал. В любом случае, вторую часть нужно заменить на sum (list), откуда приходит O (n). Формула раскрывает идею, стоящую за ней: для n последовательных целых чисел сумма должна быть (n + 1) * n / 2. Если число отсутствует, сумма будет равна сумме (n + 1) последовательных целых чисел за вычетом пропущенного числа.
Спасибо, что указали на то, что я задумал кое-что среднее.
источник
Молодцы Муравьи Аасма! Я думал над ответом около 15 минут и независимо придумал ответ в том же ключе, что и ваш:
m представляет собой «текущий максимально возможный выход с учетом того, что я знаю о первых i входах, и ничего не предполагая о значениях до входа в m-1».
Это значение m будет возвращено, только если (a [i], ..., a [m-1]) является перестановкой значений (i, ..., m-1). Таким образом, если a [i]> = m или если a [i] <i или если a [i] == a [a [i]], мы знаем, что m - неправильный результат и должен быть по крайней мере на один элемент ниже. Таким образом, уменьшая m и меняя местами a [i] на a [m], мы можем выполнить рекурсию.
Если это не так, но a [i]> i, тогда, зная, что a [i]! = A [a [i]], мы знаем, что замена a [i] на [a [i]] увеличит количество элементов на их собственном месте.
В противном случае a [i] должен быть равен i, и в этом случае мы можем увеличить i, зная, что все значения до этого индекса включительно равны их индексу.
Доказательство того, что это не может войти в бесконечный цикл, оставлено читателю в качестве упражнения. :)
источник
Dafny фрагмент из ответов показывает муравьев , почему алгоритм в месте может произойти сбой.
requires
Предварительное условие описывает , что значения каждого элемента не должны выходить за пределы массива.Вставьте код в валидатор с предложением и без него
forall ...
, чтобы увидеть ошибку проверки. Вторая ошибка является результатом того, что проверяющий не может установить условие завершения цикла Pass 1. Доказывать это остается тому, кто лучше разбирается в этом инструменте.источник
Вот ответ на Java, который не изменяет ввод и использует время O (N) и N бит плюс небольшие постоянные накладные расходы памяти (где N - размер списка):
источник
Получил 100% за решение выше.
источник
1) Фильтр отрицательный и нулевой
2) Сортировать / отличать
3) Посещение массива
Сложность : O (N) или O (N * log (N))
используя Java8
источник
Unordered_set можно использовать для хранения всех положительных чисел, а затем мы можем выполнить итерацию от 1 до длины unordered_set и увидеть первое число, которое не встречается.
источник
Решение через базовый javascript
var a = [1, 3, 6, 4, 1, 2]; function findSmallest(a) { var m = 0; for(i=1;i<=a.length;i++) { j=0;m=1; while(j < a.length) { if(i === a[j]) { m++; } j++; } if(m === 1) { return i; } } } console.log(findSmallest(a))
Надеюсь, это кому-то поможет.
источник
С питоном это не самый эффективный, но правильный
источник
источник
это может помочь:
источник