Easy interview question got harder: given numbers 1..100, find the missing number(s) given exactly k are missing

前端 未结 30 1129
时光说笑
时光说笑 2020-11-22 07:02

I had an interesting job interview experience a while back. The question started really easy:

Q1: We have a bag containing numbers

相关标签:
30条回答
  • 2020-11-22 07:57

    Yet another way is using residual graph filtering.

    Suppose we have numbers 1 to 4 and 3 is missing. The binary representation is the following,

    1 = 001b, 2 = 010b, 3 = 011b, 4 = 100b

    And I can create a flow-graph like the following.

                       1
                 1 -------------> 1
                 |                | 
          2      |     1          |
    0 ---------> 1 ----------> 0  |
    |                          |  |
    |     1            1       |  |
    0 ---------> 0 ----------> 0  |
                 |                |
          1      |      1         |
    1 ---------> 0 -------------> 1
    

    Note that the flow graph contains x nodes, while x being the number of bits. And the maximum number of edges are (2*x)-2 .

    So for 32 bit integer it will take O(32) space or O(1) space.

    Now if I remove capacity for each number starting from 1,2,4 then I am left with a residual graph.

    0 ----------> 1 ---------> 1
    

    Finally I shall run a loop like the following,

     result = []
     for x in range(1,n):
         exists_path_in_residual_graph(x)
         result.append(x)
    

    Now the result is in result contains numbers that are not missing as well(false positive). But the k <= (size of the result) <= n when there are k missing elements.

    I shall go through the given list one last time to mark the result missing or not.

    So the time complexity will be O(n) .

    Finally, it is possible to reduce the number of false positive(and the space required) by taking nodes 00,01,11,10 instead of just 0 and 1.

    0 讨论(0)
  • 2020-11-22 07:59

    To solve the 2 (and 3) missing numbers question, you can modify quickselect, which on average runs in O(n) and uses constant memory if partitioning is done in-place.

    1. Partition the set with respect to a random pivot p into partitions l, which contain numbers smaller than the pivot, and r, which contain numbers greater than the pivot.

    2. Determine which partitions the 2 missing numbers are in by comparing the pivot value to the size of each partition (p - 1 - count(l) = count of missing numbers in l and n - count(r) - p = count of missing numbers in r)

    3. a) If each partition is missing one number, then use the difference of sums approach to find each missing number.

      (1 + 2 + ... + (p-1)) - sum(l) = missing #1 and ((p+1) + (p+2) ... + n) - sum(r) = missing #2

      b) If one partition is missing both numbers and the partition is empty, then the missing numbers are either (p-1,p-2) or (p+1,p+2) depending on which partition is missing the numbers.

      If one partition is missing 2 numbers but is not empty, then recurse onto that partiton.

    With only 2 missing numbers, this algorithm always discards at least one partition, so it retains O(n) average time complexity of quickselect. Similarly, with 3 missing numbers this algorithm also discards at least one partition with each pass (because as with 2 missing numbers, at most only 1 partition will contain multiple missing numbers). However, I'm not sure how much the performance decreases when more missing numbers are added.

    Here's an implementation that does not use in-place partitioning, so this example does not meet the space requirement but it does illustrate the steps of the algorithm:

    <?php
    
      $list = range(1,100);
      unset($list[3]);
      unset($list[31]);
    
      findMissing($list,1,100);
    
      function findMissing($list, $min, $max) {
        if(empty($list)) {
          print_r(range($min, $max));
          return;
        }
    
        $l = $r = [];
        $pivot = array_pop($list);
    
        foreach($list as $number) {
          if($number < $pivot) {
            $l[] = $number;
          }
          else {
            $r[] = $number;
          }
        }
    
        if(count($l) == $pivot - $min - 1) {
          // only 1 missing number use difference of sums
          print array_sum(range($min, $pivot-1)) - array_sum($l) . "\n";
        }
        else if(count($l) < $pivot - $min) {
          // more than 1 missing number, recurse
          findMissing($l, $min, $pivot-1);
        }
    
        if(count($r) == $max - $pivot - 1) {
          // only 1 missing number use difference of sums
          print array_sum(range($pivot + 1, $max)) - array_sum($r) . "\n";
        } else if(count($r) < $max - $pivot) {
          // mroe than 1 missing number recurse
          findMissing($r, $pivot+1, $max);
        }
      }
    

    Demo

    0 讨论(0)
  • 2020-11-22 08:00

    Wait a minute. As the question is stated, there are 100 numbers in the bag. No matter how big k is, the problem can be solved in constant time because you can use a set and remove numbers from the set in at most 100 - k iterations of a loop. 100 is constant. The set of remaining numbers is your answer.

    If we generalise the solution to the numbers from 1 to N, nothing changes except N is not a constant, so we are in O(N - k) = O(N) time. For instance, if we use a bit set, we set the bits to 1 in O(N) time, iterate through the numbers, setting the bits to 0 as we go (O(N-k) = O(N)) and then we have the answer.

    It seems to me that the interviewer was asking you how to print out the contents of the final set in O(k) time rather than O(N) time. Clearly, with a bit set, you have to iterate through all N bits to determine whether you should print the number or not. However, if you change the way the set is implemented you can print out the numbers in k iterations. This is done by putting the numbers into an object to be stored in both a hash set and a doubly linked list. When you remove an object from the hash set, you also remove it from the list. The answers will be left in the list which is now of length k.

    0 讨论(0)
  • 2020-11-22 08:00

    There is a general way to generalize streaming algorithms like this. The idea is to use a bit of randomization to hopefully 'spread' the k elements into independent sub problems, where our original algorithm solves the problem for us. This technique is used in sparse signal reconstruction, among other things.

    • Make an array, a, of size u = k^2.
    • Pick any universal hash function, h : {1,...,n} -> {1,...,u}. (Like multiply-shift)
    • For each i in 1, ..., n increase a[h(i)] += i
    • For each number x in the input stream, decrement a[h(x)] -= x.

    If all of the missing numbers have been hashed to different buckets, the non-zero elements of the array will now contain the missing numbers.

    The probability that a particular pair is sent to the same bucket, is less than 1/u by definition of a universal hash function. Since there are about k^2/2 pairs, we have that the error probability is at most k^2/2/u=1/2. That is, we succeed with probability at least 50%, and if we increase u we increase our chances.

    Notice that this algorithm takes k^2 logn bits of space (We need logn bits per array bucket.) This matches the space required by @Dimitris Andreou's answer (In particular the space requirement of polynomial factorization, which happens to also be randomized.) This algorithm also has constant time per update, rather than time k in the case of power-sums.

    In fact, we can be even more efficient than the power sum method by using the trick described in the comments.

    0 讨论(0)
  • 2020-11-22 08:00

    Here is a solution that doesn't rely on complex math as sdcvvc's/Dimitris Andreou's answers do, doesn't change the input array as caf and Colonel Panic did, and doesn't use the bitset of enormous size as Chris Lercher, JeremyP and many others did. Basically, I began with Svalorzen's/Gilad Deutch's idea for Q2, generalized it to the common case Qk and implemented in Java to prove that the algorithm works.

    The idea

    Suppose we have an arbitrary interval I of which we only know that it contains at least one of the missing numbers. After one pass through the input array, looking only at the numbers from I, we can obtain both the sum S and the quantity Q of missing numbers from I. We do this by simply decrementing I's length each time we encounter a number from I (for obtaining Q) and by decreasing pre-calculated sum of all numbers in I by that encountered number each time (for obtaining S).

    Now we look at S and Q. If Q = 1, it means that then I contains only one of the missing numbers, and this number is clearly S. We mark I as finished (it is called "unambiguous" in the program) and leave it out from further consideration. On the other hand, if Q > 1, we can calculate the average A = S / Q of missing numbers contained in I. As all numbers are distinct, at least one of such numbers is strictly less than A and at least one is strictly greater than A. Now we split I in A into two smaller intervals each of which contains at least one missing number. Note that it doesn't matter to which of the intervals we assign A in case it is an integer.

    We make the next array pass calculating S and Q for each of the intervals separately (but in the same pass) and after that mark intervals with Q = 1 and split intervals with Q > 1. We continue this process until there are no new "ambiguous" intervals, i.e. we have nothing to split because each interval contains exactly one missing number (and we always know this number because we know S). We start out from the sole "whole range" interval containing all possible numbers (like [1..N] in the question).

    Time and space complexity analysis

    The total number of passes p we need to make until the process stops is never greater than the missing numbers count k. The inequality p <= k can be proved rigorously. On the other hand, there is also an empirical upper bound p < log2N + 3 that is useful for large values of k. We need to make a binary search for each number of the input array to determine the interval to which it belongs. This adds the log k multiplier to the time complexity.

    In total, the time complexity is O(N ᛫ min(k, log N) ᛫ log k). Note that for large k, this is significantly better than that of sdcvvc/Dimitris Andreou's method, which is O(N ᛫ k).

    For its work, the algorithm requires O(k) additional space for storing at most k intervals, that is significantly better than O(N) in "bitset" solutions.

    Java implementation

    Here's a Java class that implements the above algorithm. It always returns a sorted array of missing numbers. Besides that, it doesn't require the missing numbers count k because it calculates it in the first pass. The whole range of numbers is given by the minNumber and maxNumber parameters (e.g. 1 and 100 for the first example in the 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;
        }
    }
    

    For fairness, this class receives input in form of NumberBag objects. NumberBag doesn't allow array modification and random access and also counts how many times the array was requested for sequential traversing. It is also more suitable for large array testing than Iterable<Integer> because it avoids boxing of primitive int values and allows wrapping a part of a large int[] for a convenient test preparation. It is not hard to replace, if desired, NumberBag by int[] or Iterable<Integer> type in the find signature, by changing two for-loops in it into foreach ones.

    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

    Simple examples demonstrating the usage of these classes are given below.

    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");
        }
    }
    

    Large array testing can be performed this way:

    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);
        }
    }
    

    Try them out on Ideone

    0 讨论(0)
  • 2020-11-22 08:00

    You can motivate the solution by thinking about it in terms of symmetries (groups, in math language). No matter the order of the set of numbers, the answer should be the same. If you're going to use k functions to help determine the missing elements, you should be thinking about what functions have that property: symmetric. The function s_1(x) = x_1 + x_2 + ... + x_n is an example of a symmetric function, but there are others of higher degree. In particular, consider the elementary symmetric functions. The elementary symmetric function of degree 2 is s_2(x) = x_1 x_2 + x_1 x_3 + ... + x_1 x_n + x_2 x_3 + ... + x_(n-1) x_n, the sum of all products of two elements. Similarly for the elementary symmetric functions of degree 3 and higher. They are obviously symmetric. Furthermore, it turns out they are the building blocks for all symmetric functions.

    You can build the elementary symmetric functions as you go by noting that s_2(x,x_(n+1)) = s_2(x) + s_1(x)(x_(n+1)). Further thought should convince you that s_3(x,x_(n+1)) = s_3(x) + s_2(x)(x_(n+1)) and so on, so they can be computed in one pass.

    How do we tell which items were missing from the array? Think about the polynomial (z-x_1)(z-x_2)...(z-x_n). It evaluates to 0 if you put in any of the numbers x_i. Expanding the polynomial, you get z^n-s_1(x)z^(n-1)+ ... + (-1)^n s_n. The elementary symmetric functions appear here too, which is really no surprise, since the polynomial should stay the same if we apply any permutation to the roots.

    So we can build the polynomial and try to factor it to figure out which numbers are not in the set, as others have mentioned.

    Finally, if we are concerned about overflowing memory with large numbers (the nth symmetric polynomial will be of the order 100!), we can do these calculations mod p where p is a prime bigger than 100. In that case we evaluate the polynomial mod p and find that it again evaluates to 0 when the input is a number in the set, and it evaluates to a non-zero value when the input is a number not in the set. However, as others have pointed out, to get the values out of the polynomial in time that depends on k, not N, we have to factor the polynomial mod p.

    0 讨论(0)
提交回复
热议问题