Đây là một giải pháp không dựa vào toán học phức tạp như câu trả lời của sdcvvc / Dimitris Andreou, không thay đổi mảng đầu vào như quán cà phê và Đại tá Panic đã làm, và không sử dụng các bit có kích thước khổng lồ như Chris Lercher, JeremyP và nhiều người khác đã làm. Về cơ bản, tôi bắt đầu với ý tưởng của Svalorzen's / Gilad Deutch cho quý 2, khái quát nó cho trường hợp phổ biến Qk và triển khai trong Java để chứng minh rằng thuật toán hoạt động.
Ý tưởng
Giả sử chúng ta có một khoảng I tùy ý mà chúng ta chỉ biết rằng nó chứa ít nhất một trong các số còn thiếu. Sau một đi qua các mảng đầu vào, chỉ nhìn vào những con số từ tôi , chúng ta có thể có được cả hai tổng S và số lượng Q của thiếu số từ tôi . Chúng tôi thực hiện điều này bằng cách giảm độ dài của tôi mỗi lần chúng tôi gặp một số từ I (để lấy Q ) và bằng cách giảm tổng số được tính toán trước của tất cả các số trong I theo số đó gặp phải mỗi lần (để lấy S ).
Bây giờ chúng ta nhìn vào S và Q . Nếu Q = 1 , có nghĩa là sau đó tôi chỉ chứa một trong những con số mất tích, và con số này rõ ràng là S . Chúng tôi đánh dấu tôi là đã hoàn thành (nó được gọi là "không rõ ràng" trong chương trình) và để nó ra khỏi sự xem xét thêm. Mặt khác, nếu Q> 1 , chúng ta có thể tính toán mức trung bình của A = S / Q của số mất tích chứa trong tôi . Như tất cả các số là khác biệt, ít nhất một trong số đó là đúng ít hơn Một và ít nhất một là nghiêm chỉnh lớn hơn Một . Bây giờ chúng tôi chia tôi trong Athành hai khoảng nhỏ hơn, mỗi khoảng chứa ít nhất một số bị thiếu. Lưu ý rằng không quan trọng trong khoảng thời gian nào chúng ta gán A trong trường hợp đó là số nguyên.
Chúng tôi thực hiện vượt qua mảng tiếp theo tính toán S và Q cho từng khoảng riêng biệt (nhưng trong cùng một khoảng) và sau khoảng thời gian đánh dấu đó với Q = 1 và các khoảng chia với Q> 1 . Chúng tôi tiếp tục quá trình này cho đến khi không có khoảng "mơ hồ" mới, nghĩa là chúng tôi không có gì để phân chia vì mỗi khoảng chứa chính xác một số bị thiếu (và chúng tôi luôn biết số này vì chúng tôi biết S ). Chúng tôi bắt đầu từ khoảng "toàn bộ phạm vi" duy nhất chứa tất cả các số có thể (như [1..N] trong câu hỏi).
Phân tích độ phức tạp thời gian và không gian
Tổng số lần vượt qua p chúng ta cần thực hiện cho đến khi quá trình dừng lại không bao giờ lớn hơn số lượng bị thiếu k . Bất đẳng thức p <= k có thể được chứng minh một cách nghiêm ngặt. Mặt khác, cũng có một p <log 2 N + 3 theo kinh nghiệm trên hữu ích cho các giá trị lớn của k . Chúng ta cần thực hiện tìm kiếm nhị phân cho mỗi số của mảng đầu vào để xác định khoảng thời gian mà nó thuộc về. Điều này thêm số nhân log k vào độ phức tạp thời gian.
Tổng cộng, độ phức tạp thời gian là O (N ᛫ min (k, log N) log k) . Lưu ý rằng đối với k lớn , điều này tốt hơn đáng kể so với phương pháp của sdcvvc / Dimitris Andreou, đó là O (N ᛫ k) .
Đối với công việc của nó, thuật toán yêu cầu không gian bổ sung O (k) để lưu trữ tại hầu hết các khoảng k , tốt hơn đáng kể so với O (N) trong các giải pháp "bitet".
Triển khai Java
Đây là một lớp Java thực hiện thuật toán trên. Nó luôn luôn trả về một sắp xếp mảng các số mất tích. Ngoài ra, nó không yêu cầu các số bị thiếu đếm k vì nó tính toán nó trong lần đầu tiên. Toàn bộ phạm vi số được cho bởi các tham số minNumber
và maxNumber
(ví dụ 1 và 100 cho ví dụ đầu tiên trong câu hỏi).
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;
}
}
Để công bằng, lớp này nhận đầu vào dưới dạng NumberBag
đối tượng. NumberBag
không cho phép sửa đổi mảng và truy cập ngẫu nhiên và cũng tính số lần mảng được yêu cầu để duyệt tuần tự. Nó cũng phù hợp hơn cho thử nghiệm mảng lớn hơn Iterable<Integer>
vì nó tránh được các int
giá trị nguyên thủy và cho phép bọc một phần lớn int[]
để chuẩn bị kiểm tra thuận tiện. Không khó để thay thế, nếu muốn, NumberBag
bằng int[]
hoặc Iterable<Integer>
gõ find
chữ ký, bằng cách thay đổi hai vòng lặp for thành vòng lặp 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();
}
};
}
}
Xét nghiệm
Các ví dụ đơn giản thể hiện việc sử dụng các lớp này được đưa ra dưới đây.
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");
}
}
Kiểm tra mảng lớn có thể được thực hiện theo cách này:
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);
}
}
Hãy thử chúng trên Ideone