Эффективно найти целое число, не входящее в набор размером 40, 400 или 4000
В связи с классической задачей найти целое число не среди четырех миллиардов заданных, но не совсем то же самое.
Чтобы прояснить, под целыми числами я имею в виду лишь подмножество своего математического определения. То есть предположим, что существует только конечное число целых чисел. Скажем, в C++, они int
в диапазоне [INT_MIN, INT_MAX]
.
Теперь, используя std::vector<int>
(без дубликатов) или std::unordered_set<int>
, чей размер может быть 40, 400, 4000 или около того, но не слишком большой, как эффективно генерировать число, которое гарантированно не быть среди данных?
Если нет проблем с переполнением, тогда я мог бы умножить все ненулевые единицы и добавить продукт на 1. Но есть. Тестовые случаи противника могут намеренно содержать INT_MAX
.
Я больше за простые, неслучайные подходы. Есть ли?
Спасибо!
Обновление: чтобы убрать неоднозначность, скажем, несортированный std::vector<int>
который гарантированно не будет иметь дубликатов. Поэтому я спрашиваю, есть ли что-нибудь лучше, чем O (n log (n)). Также обратите внимание, что контрольные примеры могут содержать как INT_MIN
и INT_MAX
.
Ответы
Ответ 1
Вы можете просто вернуть первое из N+1
кандидатов целых чисел, не содержащихся в ваших входных данных. Простейшими кандидатами являются числа от 0
до N
Это требует O(N)
пространства и времени.
int find_not_contained(container<int> const&data)
{
const int N=data.size();
std::vector<char> known(N+1, 0); // one more candidates than data
for(int i=0; i< N; ++i)
if(data[i]>=0 && data[i]<=N)
known[data[i]]=1;
for(int i=0; i<=N; ++i)
if(!known[i])
return i;
assert(false); // should never be reached.
}
Случайные методы могут быть более компактными, но в худшем случае могут потребовать больше проходов по данным.
Ответ 2
Случайные методы действительно очень эффективны.
Если мы хотим использовать детерминированный метод и, предполагая, что размер n не слишком большой, например 4000, то мы можем создать вектор x размером m = n + 1
(или немного больше, например 4096, чтобы облегчить вычисление).), инициализируется 0.
Для каждого i
в диапазоне мы просто устанавливаем x [array [i] по модулю m] = 1.
Тогда простой поиск O (n) по x даст значение, которого нет в массиве.
Примечание: операция по модулю не совсем операция "%"
Изменение: я упомянул, что вычисления упрощаются, выбрав здесь размер 4096. Чтобы быть более конкретным, это означает, что операция по модулю выполняется с простой &
операции
Ответ 3
Вы можете найти наименьшее неиспользуемое целое число в O (N) времени, используя O (1) вспомогательное пространство, если вам разрешено изменить порядок входного вектора, используя следующий алгоритм. [Примечание 1] (Алгоритм также работает, если вектор содержит повторяющиеся данные.)
size_t smallest_unused(std::vector<unsigned>& data) {
size_t N = data.size(), scan = 0;
while (scan < N) {
auto other = data[scan];
if (other < scan && data[other] != other) {
data[scan] = data[other];
data[other] = other;
}
else
++scan;
}
for (scan = 0; scan < N && data[scan] == scan; ++scan) { }
return scan;
}
Первый проход гарантирует, что если некоторое k
в диапазоне [0, N)
было найдено после позиции k
, то оно теперь присутствует в позиции k
. Эта перестановка выполняется путем замены, чтобы избежать потери данных. Когда сканирование завершено, первая запись, значение которой не совпадает с индексом, нигде не упоминается в массиве.
Это утверждение не может быть на 100% очевидным, поскольку на запись можно ссылаться из более раннего индекса. Однако в этом случае запись не может быть первой записью, не равной ее индексу, поскольку более ранняя запись будет соответствовать этому критерию.
Чтобы увидеть, что этот алгоритм равен O (N), следует заметить, что своп в строках 6 и 7 может произойти только в том случае, если целевая запись не равна его индексу, и что после свопа целевая запись равна его индексу, Таким образом, можно выполнить не более N
перестановок, и условие if
в строке 5 будет true
не более N
раз. С другой стороны, если условие if
ложно, scan
будет увеличиваться, что также может происходить только N
раз. Таким образом, оператор if
оценивается не более 2N
раз (что равно O (N)).
Заметки:
- Я использовал здесь целые числа без знака, потому что это делает код более понятным. Алгоритм может быть легко скорректирован для целых чисел со
[INT_MIN, 0)
например, путем сопоставления целых чисел со [INT_MIN, 0)
из [INT_MIN, 0)
в целые числа без знака [INT_MAX, INT_MAX - INT_MIN)
(Вычитание является математическим, не в соответствии с семантикой C, которая не позволяет получить результат быть представленным.) В дополнении 2-х, это тот же битовый шаблон. Это, конечно, меняет порядок чисел, что влияет на семантику "наименьшего неиспользованного целого числа"; можно также использовать сохраняющее порядок отображение.
Ответ 4
Сделайте случайный x (INT_MIN..INT_MAX) и протестируйте его против всех. Проверьте x++ на неудачу (очень редкий случай для 40/400/4000).
Ответ 5
Шаг 1: Сортировка вектора.
Это можно сделать в O (n log (n)), вы можете найти несколько различных алгоритмов онлайн, используйте тот, который вам нравится больше всего.
Шаг 2: Найти первый int не в векторе.
Легко итерируйте от INT_MIN до INT_MIN + 40/400/4000, проверяя, имеет ли вектор текущее значение int:
псевдокод:
SIZE = 40|400|4000 // The one you are using
for (int i = 0; i < SIZE; i++) {
if (array[i] != INT_MIN + i)
return INT_MIN + i;
Решение будет O (n log (n) + n), что означает: O (n log (n))
Редактировать: просто прочитайте ваши изменения, прося что-то лучше, чем O (n log (n)), извините.
Ответ 6
Для случая, когда целые числа представлены в std::unordered_set<int>
(в отличие от std::vector<int>
), вы можете просто пройти диапазон целочисленных значений, пока не встретите одно целочисленное значение, которое отсутствует в unordered_set<int>
. Поиск целого числа в std::unordered_set<int>
довольно прост, поскольку std::unodered_set
обеспечивает поиск через функцию-член find()
.
Пространственная сложность этого подхода будет O (1).
Если вы начнете обходить самое низкое из возможных значений для типа int
(то есть std::numeric_limits<int>::min()
), вы получите самое низкое значение int
не содержащееся в std::unordered_set<int>
:
int find_lowest_not_contained(const std::unordered_set<int>& set) {
for (auto i = std::numeric_limits<int>::min(); ; ++i) {
auto it = set.find(i); // search in set
if (it == set.end()) // integer not in set?
return *it;
}
}
Аналогично, если вы начнете обходить максимально возможное значение для типа int
(то есть std::numeric_limits<int>::max()
), вы получите самое низкое значение int
не содержащееся в std::unordered_set<int>
:
int find_greatest_not_contained(const std::unordered_set<int>& set) {
for (auto i = std::numeric_limits<int>::max(); ; --i) {
auto it = set.find(i); // search in set
if (it == set.end()) // integer not in set?
return *it;
}
}
Предполагая, что int
равномерно отображается хэш-функцией в сегменты unordered_set
, операция поиска для unordered_set<int>
может быть выполнена за постоянное время. В этом случае сложность во время выполнения будет равна O (M), где M - это размер целочисленного диапазона, который вы ищете для не содержащего значения. M ограничен сверху размером unordered_set<int>
(т.е. В вашем случае M <= 4000).
Действительно, при таком подходе выбор любого целочисленного диапазона, размер которого больше размера unordered_set
, гарантированно натолкнется на целочисленное значение, которого нет в unordered_set<int>
.