これは、sdcvvc / Dimitris Andreouの回答のように複雑な数学に依存せず、cafやパネル大佐が行ったように入力配列を変更せず、Chris Lercher、JeremyP、および他の多くがしました。基本的に、私はQ2に関するSvalorzen / Gilad Deutchのアイデアから始め、それを一般的なケースQkに一般化し、Javaで実装して、アルゴリズムが機能することを証明しました。
アイデア
任意の間隔Iがあり、そこに少なくとも1つの欠落した数値が含まれていることがわかっていると仮定します。入力配列を1回通過した後、Iからの数値のみを見ると、Iから欠落している数値の合計Sと数量Qの両方を取得できます。私たちは、単に減算することによってこれを行うIの長たちから数に遭遇するたびにI(取得するためのQを)とのすべての数字の前に計算合計を減少させることによってIその遭遇数(取得するための各時間でSを)。
次にSとQを見てみましょう。場合Q = 1、それはその後、ということを意味し、私は一つだけ欠番のが含まれており、この数は、明らかであるS。私は私に終了済みのマークを付け(プログラムでは「あいまいでない」と呼ばれます)、それ以上の検討から除外します。一方、Q> 1の場合、Iに含まれる欠落した数値の平均A = S / Qを計算できます。すべての数値が異なるため、そのような数値の少なくとも1つはAよりも厳密に小さく、少なくとも1つはAよりも厳密に大きくなります。今私たちは私をAに分割しますそれぞれが少なくとも1つの欠落している数を含む2つの小さな間隔に。整数の場合、どの間隔にAを割り当てるかは問題ではないことに注意してください。
次の配列パスでは、間隔ごとにSとQを個別に(ただし同じパスで)計算し、その後、マーク間隔をQ = 1に、分割間隔をQ> 1に設定します。新しい「あいまいな」区間がなくなるまでこのプロセスを続けます。つまり、各区間には正確に1つの欠落した数値が含まれるため、分割するものは何もありません(そしてSを知っているので、常にこの数値を知っています)。すべての可能な数値を含む唯一の「全範囲」の間隔から始めます(質問の[1..N]など)。
時間と空間の複雑さの分析
プロセスが停止するまでに必要なパスの総数pは、欠落数カウントkより大きくなることはありません。不等式p <= kは厳密に証明できます。一方、kの大きな値に役立つ経験的な上限p <log 2 N + 3もあります。入力配列が属する間隔を決定するために、入力配列の各番号に対してバイナリ検索を行う必要があります。これにより、時間の複雑さにlog k乗数が追加されます。
全体として、時間の複雑さはO(N᛫min(k、log N)᛫log k)です。大きなkの場合、これはO(N᛫k)であるsdcvvc / Dimitris Andreouの方法よりもはるかに優れていることに注意してください。
その作業のために、アルゴリズムは最大k間隔で格納するためにO(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>タイプfindforeachのものにするためのループ、それに2つを変更することにより、署名。
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);
    }
}
イデオーネでお試しください