Aquí hay una solución que no se basa en matemáticas complejas como lo hacen las respuestas de sdcvvc / Dimitris Andreou, no cambia la matriz de entrada como lo hicieron caf y Coronel Panic, y no usa el bitset de enorme tamaño como Chris Lercher, JeremyP y muchos otros lo hicieron. Básicamente, comencé con la idea de Svalorzen / Gilad Deutch para Q2, la generalicé al caso común Qk y la implementé en Java para demostrar que el algoritmo funciona.
La idea
Supongamos que tenemos un intervalo arbitrario I del cual solo sabemos que contiene al menos uno de los números que faltan. Después de una pasada a través de la matriz de entrada, mirando sólo los números de I , podemos obtener tanto la suma S y la cantidad Q de los números que falta de I . Hacemos esto simplemente disminuyendo la longitud de I cada vez que encontramos un número de I (para obtener Q ) y disminuyendo la suma calculada previamente de todos los números en I por ese número encontrado cada vez (para obtener S ).
Ahora nos centraremos en S y Q . Si Q = 1 , significa que entonces me contiene sólo uno de los números que faltan, y este número es claramente S . Marcamos I como terminado (se llama "inequívoco" en el programa) y lo dejamos fuera de consideración. Por otro lado, si Q> 1 , se puede calcular el promedio A = S / Q de los números que faltan contenida en I . Como todos los números son distintos, al menos uno de dichos números es estrictamente menor que A y al menos una es estrictamente mayor que A . Ahora nos separamos yo en unaen dos intervalos más pequeños, cada uno de los cuales contiene al menos un número faltante. Tenga en cuenta que no importa a cuál de los intervalos asignamos A en caso de que sea un número entero.
Realizamos el siguiente paso de matriz calculando S y Q para cada uno de los intervalos por separado (pero en el mismo paso) y luego marcamos los intervalos con Q = 1 y los intervalos divididos con Q> 1 . Continuamos este proceso hasta que no haya nuevos intervalos "ambiguos", es decir, no tenemos nada que dividir porque cada intervalo contiene exactamente un número faltante (y siempre sabemos este número porque sabemos S ). Comenzamos desde el único intervalo de "rango completo" que contiene todos los números posibles (como [1..N] en la pregunta).
Análisis de complejidad de tiempo y espacio.
El número total de pases p que necesitamos hacer hasta que el proceso se detenga nunca es mayor que los números faltantes cuentan k . La desigualdad p <= k puede demostrarse rigurosamente. Por otro lado, también hay un límite superior empírico p <log 2 N + 3 que es útil para valores grandes de k . Necesitamos hacer una búsqueda binaria para cada número de la matriz de entrada para determinar el intervalo al que pertenece. Esto agrega el multiplicador log k a la complejidad del tiempo.
En total, la complejidad del tiempo es O (N ᛫ min (k, log N) ᛫ log k) . Tenga en cuenta que para k grande , esto es significativamente mejor que el del método sdcvvc / Dimitris Andreou, que es O (N ᛫ k) .
Para su trabajo, el algoritmo requiere O (k) de espacio adicional para almacenar en la mayoría de los intervalos de k , que es significativamente mejor que O (N) en soluciones de "conjunto de bits".
Implementación de Java
Aquí hay una clase de Java que implementa el algoritmo anterior. Siempre devuelve una matriz ordenada de números faltantes. Además de eso, no requiere que los números que faltan cuenten k porque lo calcula en la primera pasada. El rango completo de números viene dado por los parámetros minNumber
y maxNumber
(por ejemplo, 1 y 100 para el primer ejemplo en la pregunta).
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;
}
}
Para ser justos, esta clase recibe información en forma de NumberBag
objetos. NumberBag
no permite la modificación de la matriz y el acceso aleatorio y también cuenta cuántas veces se solicitó la matriz para el recorrido secuencial. También es más adecuado para pruebas de matriz grande que Iterable<Integer>
porque evita el encajonamiento de int
valores primitivos y permite envolver una parte de una gran int[]
para una preparación de prueba conveniente. No es difícil de reemplazar, en caso dado, NumberBag
por int[]
o Iterable<Integer>
escribir en la find
firma, mediante el cambio de dos bucles en que foreach en otros más.
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();
}
};
}
}
Pruebas
A continuación se dan ejemplos simples que demuestran el uso de estas clases.
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");
}
}
Las pruebas de matriz grande se pueden realizar de esta manera:
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);
}
}
Pruébalos en Ideone