Voici une solution qui ne repose pas sur des mathématiques complexes comme le font les réponses de sdcvvc / Dimitris Andreou, ne change pas le tableau d'entrée comme l'ont fait caf et le colonel Panic, et n'utilise pas le jeu de bits de taille énorme comme Chris Lercher, JeremyP et beaucoup d'autres l'ont fait. Fondamentalement, j'ai commencé avec l'idée de Svalorzen / Gilad Deutch pour Q2, je l'ai généralisée au cas courant Qk et implémentée en Java pour prouver que l'algorithme fonctionne.
L'idée
Supposons que nous ayons un intervalle arbitraire I dont nous savons seulement qu'il contient au moins un des nombres manquants. Après un passage à travers le réseau d'entrée, ne regardant que les chiffres de I , on peut obtenir à la fois la somme S et la quantité Q de chiffres manquants I . Nous faisons cela en décrémentant simplement la longueur de I à chaque fois que nous rencontrons un nombre de I (pour obtenir Q ) et en diminuant la somme pré-calculée de tous les nombres de I par ce nombre rencontré à chaque fois (pour obtenir S ).
Maintenant , nous regardons S et Q . Si Q = 1 , cela signifie que alors je ne compte qu'un des numéros manquants, et ce nombre est clairement S . Nous marquons I comme terminé (il est appelé "sans ambiguïté" dans le programme) et le laissons de côté. D'autre part, si Q> 1 , on peut calculer la moyenne A = S / Q des nombres manquants contenus dans I . Comme tous les nombres sont distincts, au moins l' un de ces nombres est strictement inférieur à A et au moins un est strictement supérieur à un . Maintenant, nous avons divisé I en Aen deux intervalles plus petits dont chacun contient au moins un nombre manquant. Notez que peu importe à quels intervalles nous attribuons A au cas où il s'agit d'un entier.
Nous faisons la passe de tableau suivante calculant S et Q pour chacun des intervalles séparément (mais dans la même passe) et après cela, marquons les intervalles avec Q = 1 et divisons les intervalles avec Q> 1 . Nous continuons ce processus jusqu'à ce qu'il n'y ait pas de nouveaux intervalles "ambigus", c'est-à-dire que nous n'avons rien à diviser car chaque intervalle contient exactement un nombre manquant (et nous connaissons toujours ce nombre parce que nous connaissons S ). Nous partons du seul intervalle "plage entière" contenant tous les nombres possibles (comme [1..N] dans la question).
Analyse de la complexité du temps et de l'espace
Le nombre total de passes p que nous devons effectuer jusqu'à l'arrêt du processus n'est jamais supérieur au nombre manquant de k . L'inégalité p <= k peut être rigoureusement démontrée. D'autre part, il existe également une borne supérieure empirique p <log 2 N + 3 qui est utile pour les grandes valeurs de k . Nous devons effectuer une recherche binaire pour chaque numéro du tableau d'entrée pour déterminer l'intervalle auquel il appartient. Cela ajoute le multiplicateur log k à la complexité temporelle.
Au total, la complexité temporelle est O (N ᛫ min (k, log N) ᛫ log k) . Notez que pour les grands k , c'est nettement mieux que celui de la méthode de sdcvvc / Dimitris Andreou, qui est O (N ᛫ k) .
Pour son travail, l'algorithme nécessite O (k) d' espace supplémentaire pour le stockage à la plupart des k intervalles, ce qui est nettement meilleur que O (N) dans les solutions de "bits".
Implémentation Java
Voici une classe Java qui implémente l'algorithme ci-dessus. Il renvoie toujours un tableau trié de nombres manquants. En plus de cela, il ne nécessite pas le nombre k manquant car il le calcule lors de la première passe. L'ensemble des nombres est donné par les paramètres minNumber
et maxNumber
(par exemple 1 et 100 pour le premier exemple de la question).
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;
}
}
Par souci d'équité, cette classe reçoit des données sous forme d' NumberBag
objets. NumberBag
ne permet pas la modification du tableau et l'accès aléatoire et compte également le nombre de fois que le tableau a été demandé pour une traversée séquentielle. Il est également plus approprié pour les tests de grands tableaux que Iterable<Integer>
parce qu'il évite la mise en boîte de int
valeurs primitives et permet d'envelopper une partie d'un grand int[]
pour une préparation de test pratique. Il n'est pas difficile de remplacer, si vous le souhaitez, NumberBag
par int[]
ou de Iterable<Integer>
taper la find
signature, en y changeant deux boucles for en boucles 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();
}
};
}
}
Les tests
Des exemples simples démontrant l'utilisation de ces classes sont donnés ci-dessous.
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");
}
}
Les tests de grande baie peuvent être effectués de cette façon:
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);
}
}
Essayez-les sur Ideone