Hier ist eine Lösung, die sich nicht auf komplexe Mathematik stützt wie die Antworten von sdcvvc / Dimitris Andreou, das Eingabearray nicht wie bei caf und Colonel Panic ändert und das Bitset von enormer Größe nicht wie Chris Lercher, JeremyP und verwendet viele andere taten es. Grundsätzlich begann ich mit der Idee von Svalorzen / Gilad Deutch für Q2, verallgemeinerte sie auf den allgemeinen Fall Qk und implementierte sie in Java, um zu beweisen, dass der Algorithmus funktioniert.
Die Idee
Angenommen, wir haben ein beliebiges Intervall I, von dem wir nur wissen, dass es mindestens eine der fehlenden Zahlen enthält. Nach einem Durchgang durch das Eingabearray , wobei wir nur die Zahlen von I betrachten , können wir sowohl die Summe S als auch die Menge Q der fehlenden Zahlen von I erhalten . Wir tun dies, indem wir einfach die Länge von I jedes Mal verringern, wenn wir auf eine Zahl von I stoßen (um Q zu erhalten ) und indem wir die vorberechnete Summe aller Zahlen in I jedes Mal um diese angetroffene Zahl verringern (um S zu erhalten ).
Nun schauen wir uns S und Q an . Wenn Q = 1 , bedeutet dies , dass dann ich enthalten nur eine der fehlenden Zahlen, und diese Zahl ist deutlich S . Wir markieren I als fertig (es wird im Programm als "eindeutig" bezeichnet) und lassen es von weiteren Überlegungen aus. Wenn andererseits Q> 1 ist , können wir den Durchschnitt A = S / Q der in I enthaltenen fehlenden Zahlen berechnen . Da alle Zahlen verschieden sind, wobei mindestens eine dieser Zahlen ist strikt kleiner als A und mindestens ein streng größer als ist A . Jetzt teilen wir mich in A.in zwei kleinere Intervalle, von denen jedes mindestens eine fehlende Zahl enthält. Beachten Sie, dass es keine Rolle spielt, welchem der Intervalle wir A zuweisen, falls es sich um eine Ganzzahl handelt.
Wir machen den nächsten Array-Durchgang, indem wir S und Q für jedes der Intervalle separat berechnen (aber im selben Durchgang) und danach Intervalle mit Q = 1 markieren und Intervalle mit Q> 1 teilen . Wir setzen diesen Prozess fort, bis es keine neuen "mehrdeutigen" Intervalle mehr gibt, dh wir haben nichts zu teilen, da jedes Intervall genau eine fehlende Zahl enthält (und wir kennen diese Zahl immer, weil wir S kennen ). Wir beginnen mit dem einzigen "gesamten Bereich" -Intervall, das alle möglichen Zahlen enthält (wie [1..N] in der Frage).
Zeit- und Raumkomplexitätsanalyse
Die Gesamtzahl der Durchgänge p, die wir machen müssen, bis der Prozess stoppt, ist niemals größer als die Anzahl der fehlenden Zahlen k . Die Ungleichung p <= k kann rigoros bewiesen werden. Andererseits gibt es auch eine empirische Obergrenze p <log 2 N + 3 , die für große Werte von k nützlich ist . Wir müssen eine binäre Suche für jede Nummer des Eingabearrays durchführen, um das Intervall zu bestimmen, zu dem es gehört. Dies addiert den log k- Multiplikator zur Zeitkomplexität.
Insgesamt beträgt die zeitliche Komplexität O (N ≤ min (k, log N) ≤ log k) . Beachten Sie, dass dies für großes k signifikant besser ist als für die Methode von sdcvvc / Dimitris Andreou, die O (N ᛫ k) ist .
Für seine Arbeit benötigt der Algorithmus O (k) zusätzlichen Speicherplatz zum Speichern in den meisten k Intervallen, was in "Bitset" -Lösungen signifikant besser ist als O (N) .
Java-Implementierung
Hier ist eine Java-Klasse, die den obigen Algorithmus implementiert. Es wird immer ein sortiertes Array fehlender Zahlen zurückgegeben. Außerdem müssen die fehlenden Zahlen nicht k gezählt werden, da sie im ersten Durchgang berechnet werden. Der gesamte Zahlenbereich wird durch die Parameter minNumber
und maxNumber
angegeben (z. B. 1 und 100 für das erste Beispiel in der Frage).
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;
}
}
Aus Fairnessgründen erhält diese Klasse Eingaben in Form von NumberBag
Objekten. NumberBag
Ermöglicht keine Änderung des Arrays und keinen wahlfreien Zugriff und zählt auch, wie oft das Array zum sequentiellen Durchlaufen angefordert wurde. Es ist auch besser für Tests mit großen Arrays geeignet, als Iterable<Integer>
weil es das Boxen primitiver int
Werte vermeidet und das Umwickeln eines Teils eines großen int[]
Werts für eine bequeme Testvorbereitung ermöglicht. Es ist nicht schwer zu ersetzen, falls gewünscht, NumberBag
durch int[]
oder Iterable<Integer>
in dem Typ find
Signatur, die von zwei wechselnden for-Schleifen in ihnen in foreach denjenigen.
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();
}
};
}
}
Tests
Nachfolgend finden Sie einfache Beispiele für die Verwendung dieser Klassen.
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");
}
}
Tests mit großen Arrays können folgendermaßen durchgeführt werden:
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);
}
}
Probieren Sie sie auf Ideone aus