Ось рішення, яке не покладається на складну математику, як це роблять відповіді sdcvvc / Димитріс Андреу, не змінює вхідний масив, як це робив caf і полковник Паніка, і не використовує біт величезних розмірів, як 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 . Тепер ми розділили І в Ана два менші інтервали, кожен з яких містить щонайменше одне відсутнє число. Зауважте, що не має значення, якому з інтервалів ми призначимо 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 / Димитріса Андреу, який є 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