Вот простое 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
битов в худшем и лучшем случаях.
Однако этот анализ приводит к интуиции, что если бы алгоритм произвел первоначальный проход по списку в поисках нуля (и подсчитал элементы списка, если необходимо), он дал бы более быстрый ответ, не используя пробела вообще, если бы он нашел ноль. Это однозначно стоит сделать, если велика вероятность найти хотя бы один ноль в списке. И этот дополнительный проход не меняет общей сложности.
РЕДАКТИРОВАТЬ: я изменил описание алгоритма, чтобы использовать «массив логических значений», поскольку люди, по-видимому, сочли мое исходное описание с использованием битов и растровых изображений запутанным.