Вот решение, которое не полагается на сложную математику, как это делают ответы sdcvvc / Dimitris Andreou, не меняет входной массив, как это сделали caf и полковник Panic, и не использует набор огромных размеров, как Chris Lercher, JeremyP и многие другие сделали. По сути, я начал с идеи Свалорцена / Гилада Дойча для Q2, обобщил ее для общего случая Qk и реализовал в Java, чтобы доказать, что алгоритм работает.
Идея
Предположим, у нас есть произвольный интервал I, из которого мы знаем только то, что он содержит хотя бы одно из пропущенных чисел. После того, как один проход через входной массив, только глядя на цифры от I , можно получить как сумму S и величину Q недостающих чисел от I . Мы делаем это, просто уменьшая длину I каждый раз, когда мы встречаем число от I (для получения Q ), и уменьшая предварительно вычисленную сумму всех чисел в I на это число каждый раз (для получения S ).
Теперь мы смотрим на S и Q . Если Q = 1 , то это означает , что тогда я содержит только один из недостающих чисел, и это число явно S . Мы помечаем I как завершенный (в программе это называется «однозначным») и оставляем его для дальнейшего рассмотрения. С другой стороны, если Q> 1 , мы можем вычислить среднее A = S / Q недостающих чисел содержится в I . Поскольку все числа различны, по крайней мере , один из таких чисел строго меньше , чем А и по меньшей мере один строго больше , чем A . Теперь мы разобьем I в Ана два меньших интервала, каждый из которых содержит хотя бы одно пропущенное число. Обратите внимание, что не имеет значения, какому из интервалов мы назначаем A, если это целое число.
Мы делаем следующий проход массива, вычисляя S и Q для каждого из интервалов отдельно (но в том же проходе), и после этого отмечаем интервалы с Q = 1 и разделяем интервалы с Q> 1 . Мы продолжаем этот процесс до тех пор, пока не появятся новые «неоднозначные» интервалы, то есть нам нечего делить, потому что каждый интервал содержит ровно одно пропущенное число (и мы всегда знаем это число, потому что мы знаем S ). Мы начинаем с единственного интервала «весь диапазон», содержащего все возможные числа (как [1..N] в вопросе).
Анализ сложности времени и пространства
Общее количество проходов p, которое мы должны сделать до тех пор, пока процесс не остановится, никогда не превысит число пропущенных чисел k . Неравенство p <= k может быть строго доказано. С другой стороны, существует также эмпирическая верхняя граница p <log 2 N + 3, которая полезна для больших значений k . Нам нужно выполнить бинарный поиск для каждого номера входного массива, чтобы определить интервал, к которому он принадлежит. Это добавляет множитель log k к сложности времени.
В общей сложности сложность по времени составляет O (N ≤ min (k, log N) ᛫ log k) . Обратите внимание, что для больших k это значительно лучше, чем для метода sdcvvc / Dimitris Andreou, который является O (N ᛫ k) .
Для своей работы алгоритм требует O (k) дополнительного пространства для хранения в большинстве k интервалов, что значительно лучше, чем O (N) в «битовых» решениях.
Реализация Java
Вот класс Java, который реализует вышеупомянутый алгоритм. Он всегда возвращает отсортированный массив пропущенных чисел. Кроме того, он не требует подсчета пропущенных чисел k, потому что вычисляет его при первом проходе. Весь диапазон чисел задается minNumber
и maxNumber
параметры (например , 1 и 100 для первого примера в вопросе).
public class MissingNumbers {
private static class Interval {
boolean ambiguous = true;
final int begin;
int quantity;
long sum;
Interval(int begin, int end) { // begin inclusive, end exclusive
this.begin = begin;
quantity = end - begin;
sum = quantity * ((long)end - 1 + begin) / 2;
}
void exclude(int x) {
quantity--;
sum -= x;
}
}
public static int[] find(int minNumber, int maxNumber, NumberBag inputBag) {
Interval full = new Interval(minNumber, ++maxNumber);
for (inputBag.startOver(); inputBag.hasNext();)
full.exclude(inputBag.next());
int missingCount = full.quantity;
if (missingCount == 0)
return new int[0];
Interval[] intervals = new Interval[missingCount];
intervals[0] = full;
int[] dividers = new int[missingCount];
dividers[0] = minNumber;
int intervalCount = 1;
while (true) {
int oldCount = intervalCount;
for (int i = 0; i < oldCount; i++) {
Interval itv = intervals[i];
if (itv.ambiguous)
if (itv.quantity == 1) // number inside itv uniquely identified
itv.ambiguous = false;
else
intervalCount++; // itv will be split into two intervals
}
if (oldCount == intervalCount)
break;
int newIndex = intervalCount - 1;
int end = maxNumber;
for (int oldIndex = oldCount - 1; oldIndex >= 0; oldIndex--) {
// newIndex always >= oldIndex
Interval itv = intervals[oldIndex];
int begin = itv.begin;
if (itv.ambiguous) {
// split interval itv
// use floorDiv instead of / because input numbers can be negative
int mean = (int)Math.floorDiv(itv.sum, itv.quantity) + 1;
intervals[newIndex--] = new Interval(mean, end);
intervals[newIndex--] = new Interval(begin, mean);
} else
intervals[newIndex--] = itv;
end = begin;
}
for (int i = 0; i < intervalCount; i++)
dividers[i] = intervals[i].begin;
for (inputBag.startOver(); inputBag.hasNext();) {
int x = inputBag.next();
// find the interval to which x belongs
int i = java.util.Arrays.binarySearch(dividers, 0, intervalCount, x);
if (i < 0)
i = -i - 2;
Interval itv = intervals[i];
if (itv.ambiguous)
itv.exclude(x);
}
}
assert intervalCount == missingCount;
for (int i = 0; i < intervalCount; i++)
dividers[i] = (int)intervals[i].sum;
return dividers;
}
}
Справедливости ради, этот класс получает входные данные в виде NumberBag
объектов. NumberBag
не разрешает изменение массива и произвольный доступ, а также подсчитывает, сколько раз массив был запрошен для последовательного обхода. Он также больше подходит для тестирования большого массива, чем Iterable<Integer>
потому, что он позволяет избежать int
упаковки примитивных значений и позволяет обернуть часть большого int[]
для удобной подготовки теста. Это не трудно заменить, если это необходимо, с NumberBag
помощью int[]
или Iterable<Integer>
введите в find
подписи, изменив два для лупов в нем в Foreach из них.
import java.util.*;
public abstract class NumberBag {
private int passCount;
public void startOver() {
passCount++;
}
public final int getPassCount() {
return passCount;
}
public abstract boolean hasNext();
public abstract int next();
// A lightweight version of Iterable<Integer> to avoid boxing of int
public static NumberBag fromArray(int[] base, int fromIndex, int toIndex) {
return new NumberBag() {
int index = toIndex;
public void startOver() {
super.startOver();
index = fromIndex;
}
public boolean hasNext() {
return index < toIndex;
}
public int next() {
if (index >= toIndex)
throw new NoSuchElementException();
return base[index++];
}
};
}
public static NumberBag fromArray(int[] base) {
return fromArray(base, 0, base.length);
}
public static NumberBag fromIterable(Iterable<Integer> base) {
return new NumberBag() {
Iterator<Integer> it;
public void startOver() {
super.startOver();
it = base.iterator();
}
public boolean hasNext() {
return it.hasNext();
}
public int next() {
return it.next();
}
};
}
}
тесты
Простые примеры, демонстрирующие использование этих классов, приведены ниже.
import java.util.*;
public class SimpleTest {
public static void main(String[] args) {
int[] input = { 7, 1, 4, 9, 6, 2 };
NumberBag bag = NumberBag.fromArray(input);
int[] output = MissingNumbers.find(1, 10, bag);
System.out.format("Input: %s%nMissing numbers: %s%nPass count: %d%n",
Arrays.toString(input), Arrays.toString(output), bag.getPassCount());
List<Integer> inputList = new ArrayList<>();
for (int i = 0; i < 10; i++)
inputList.add(2 * i);
Collections.shuffle(inputList);
bag = NumberBag.fromIterable(inputList);
output = MissingNumbers.find(0, 19, bag);
System.out.format("%nInput: %s%nMissing numbers: %s%nPass count: %d%n",
inputList, Arrays.toString(output), bag.getPassCount());
// Sieve of Eratosthenes
final int MAXN = 1_000;
List<Integer> nonPrimes = new ArrayList<>();
nonPrimes.add(1);
int[] primes;
int lastPrimeIndex = 0;
while (true) {
primes = MissingNumbers.find(1, MAXN, NumberBag.fromIterable(nonPrimes));
int p = primes[lastPrimeIndex]; // guaranteed to be prime
int q = p;
for (int i = lastPrimeIndex++; i < primes.length; i++) {
q = primes[i]; // not necessarily prime
int pq = p * q;
if (pq > MAXN)
break;
nonPrimes.add(pq);
}
if (q == p)
break;
}
System.out.format("%nSieve of Eratosthenes. %d primes up to %d found:%n",
primes.length, MAXN);
for (int i = 0; i < primes.length; i++)
System.out.format(" %4d%s", primes[i], (i % 10) < 9 ? "" : "\n");
}
}
Тестирование большого массива может быть выполнено следующим образом:
import java.util.*;
public class BatchTest {
private static final Random rand = new Random();
public static int MIN_NUMBER = 1;
private final int minNumber = MIN_NUMBER;
private final int numberCount;
private final int[] numbers;
private int missingCount;
public long finderTime;
public BatchTest(int numberCount) {
this.numberCount = numberCount;
numbers = new int[numberCount];
for (int i = 0; i < numberCount; i++)
numbers[i] = minNumber + i;
}
private int passBound() {
int mBound = missingCount > 0 ? missingCount : 1;
int nBound = 34 - Integer.numberOfLeadingZeros(numberCount - 1); // ceil(log_2(numberCount)) + 2
return Math.min(mBound, nBound);
}
private void error(String cause) {
throw new RuntimeException("Error on '" + missingCount + " from " + numberCount + "' test, " + cause);
}
// returns the number of times the input array was traversed in this test
public int makeTest(int missingCount) {
this.missingCount = missingCount;
// numbers array is reused when numberCount stays the same,
// just Fisher–Yates shuffle it for each test
for (int i = numberCount - 1; i > 0; i--) {
int j = rand.nextInt(i + 1);
if (i != j) {
int t = numbers[i];
numbers[i] = numbers[j];
numbers[j] = t;
}
}
final int bagSize = numberCount - missingCount;
NumberBag inputBag = NumberBag.fromArray(numbers, 0, bagSize);
finderTime -= System.nanoTime();
int[] found = MissingNumbers.find(minNumber, minNumber + numberCount - 1, inputBag);
finderTime += System.nanoTime();
if (inputBag.getPassCount() > passBound())
error("too many passes (" + inputBag.getPassCount() + " while only " + passBound() + " allowed)");
if (found.length != missingCount)
error("wrong result length");
int j = bagSize; // "missing" part beginning in numbers
Arrays.sort(numbers, bagSize, numberCount);
for (int i = 0; i < missingCount; i++)
if (found[i] != numbers[j++])
error("wrong result array, " + i + "-th element differs");
return inputBag.getPassCount();
}
public static void strideCheck(int numberCount, int minMissing, int maxMissing, int step, int repeats) {
BatchTest t = new BatchTest(numberCount);
System.out.println("╠═══════════════════════╬═════════════════╬═════════════════╣");
for (int missingCount = minMissing; missingCount <= maxMissing; missingCount += step) {
int minPass = Integer.MAX_VALUE;
int passSum = 0;
int maxPass = 0;
t.finderTime = 0;
for (int j = 1; j <= repeats; j++) {
int pCount = t.makeTest(missingCount);
if (pCount < minPass)
minPass = pCount;
passSum += pCount;
if (pCount > maxPass)
maxPass = pCount;
}
System.out.format("║ %9d %9d ║ %2d %5.2f %2d ║ %11.3f ║%n", missingCount, numberCount, minPass,
(double)passSum / repeats, maxPass, t.finderTime * 1e-6 / repeats);
}
}
public static void main(String[] args) {
System.out.println("╔═══════════════════════╦═════════════════╦═════════════════╗");
System.out.println("║ Number count ║ Passes ║ Average time ║");
System.out.println("║ missimg total ║ min avg max ║ per search (ms) ║");
long time = System.nanoTime();
strideCheck(100, 0, 100, 1, 20_000);
strideCheck(100_000, 2, 99_998, 1_282, 15);
MIN_NUMBER = -2_000_000_000;
strideCheck(300_000_000, 1, 10, 1, 1);
time = System.nanoTime() - time;
System.out.println("╚═══════════════════════╩═════════════════╩═════════════════╝");
System.out.format("%nSuccess. Total time: %.2f s.%n", time * 1e-9);
}
}
Попробуйте их на Ideone