Ecco una soluzione che non si basa su una matematica complessa come fanno le risposte di sdcvvc / Dimitris Andreou, non cambia l'array di input come hanno fatto caf e il colonnello Panic e non usa un bit di dimensioni enormi come Chris Lercher, JeremyP e molti altri hanno fatto. Fondamentalmente, ho iniziato con l'idea di Svalorzen / Gilad Deutch per Q2, l'ho generalizzato al caso comune Qk e implementato in Java per dimostrare che l'algoritmo funziona.
L'idea
Supponiamo di avere un intervallo arbitrario I di cui sappiamo solo che contiene almeno uno dei numeri mancanti. Dopo un passaggio attraverso l'array di input, guardando solo i numeri da Io , possiamo ottenere sia la somma S e la quantità Q di numeri mancanti da I . Lo facciamo semplicemente diminuendo la lunghezza di I ogni volta che incontriamo un numero da I (per ottenere Q ) e diminuendo la somma precalcolata di tutti i numeri in I di quel numero rilevato ogni volta (per ottenere S ).
Ora guardiamo S e Q . Se Q = 1 , significa che allora mi compare soltanto uno dei numeri mancanti, e questo numero è chiaramente S . Segniamo I come finito (nel programma è chiamato "non ambiguo") e lo tralasciamo da ulteriori considerazioni. D'altra parte, se Q> 1 , siamo in grado di calcolare la media A = S / Q dei numeri mancanti contenuto in I . Poiché tutti i numeri sono distinti, almeno uno di questi numeri è strettamente minore di A e almeno uno è strettamente maggiore di A . Ora abbiamo diviso I in Ain due intervalli più piccoli ciascuno dei quali contiene almeno un numero mancante. Nota che non importa a quale degli intervalli assegniamo A nel caso in cui sia un numero intero.
Effettuiamo il passaggio dell'array successivo calcolando S e Q per ciascuno degli intervalli separatamente (ma nello stesso passaggio) e successivamente contrassegniamo gli intervalli con Q = 1 e dividiamo gli intervalli con Q> 1 . Continuiamo questo processo fino a quando non ci sono nuovi intervalli "ambigui", cioè non abbiamo nulla da dividere perché ogni intervallo contiene esattamente un numero mancante (e conosciamo sempre questo numero perché conosciamo S ). Partiamo dall'unico intervallo "intero intervallo" contenente tutti i possibili numeri (come [1..N] nella domanda).
Analisi della complessità del tempo e dello spazio
Il numero totale di passaggi p che dobbiamo effettuare fino a quando il processo si arresta non è mai maggiore del conteggio dei numeri mancanti k . La disuguaglianza p <= k può essere dimostrata rigorosamente. D'altra parte, esiste anche un limite superiore empirico p <log 2 N + 3 che è utile per grandi valori di k . È necessario effettuare una ricerca binaria per ciascun numero dell'array di input per determinare l'intervallo a cui appartiene. Ciò aggiunge il moltiplicatore log k alla complessità temporale.
In totale, la complessità temporale è O (N ᛫ min (k, log N) ᛫ log k) . Nota che per k di grandi dimensioni , questo è significativamente migliore rispetto a quello del metodo sdcvvc / Dimitris Andreou, che è O (N ᛫ k) .
Per il suo lavoro, l'algoritmo richiede O (k) spazio aggiuntivo per la memorizzazione alla maggior parte degli intervalli di k , che è significativamente migliore di O (N) nelle soluzioni "bitset".
Implementazione Java
Ecco una classe Java che implementa l'algoritmo sopra. Restituisce sempre una matrice ordinata di numeri mancanti. Oltre a ciò, non richiede che i numeri mancanti contino k perché lo calcola al primo passaggio. L'intero intervallo di numeri è dato dai parametri minNumber
e maxNumber
(es. 1 e 100 per il primo esempio nella domanda).
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;
}
}
Per onestà, questa classe riceve input sotto forma di NumberBag
oggetti. NumberBag
non consente la modifica dell'array e l'accesso casuale e conta anche quante volte l'array è stato richiesto per l'attraversamento sequenziale. È anche più adatto per test di array di grandi dimensioni che Iterable<Integer>
perché evita l'inscatolamento di int
valori primitivi e consente di avvolgere una parte di un grande int[]
per una comoda preparazione del test. Non è difficile sostituire, se lo si desidera, NumberBag
con int[]
o Iterable<Integer>
digitare la find
firma, cambiando in essa due for-loop in 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();
}
};
}
}
test
Di seguito sono riportati semplici esempi che dimostrano l'utilizzo di queste classi.
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");
}
}
In questo modo è possibile eseguire test di array di grandi dimensioni:
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);
}
}
Provali su Ideone