14

Let's say I have a list

ArrayList<String> arr = new ArrayList(Arrays.asList("N1", "N2", "N3", "N5"));

How do I find "N4", I mean, how I find that the missing integer is 4?

What I've tried so far

Integer missingID = arr.stream().map(p -> Integer.parseInt(p.substring(1))).sorted()
                .reduce((p1, p2) -> (p2 - p1) > 1 ? p1 + 1 : 0).get();

This doesn't work because reduce is not intended to work in the way I need in this situation, actually, I have no idea how do that. If there's no missing number, than the next must be "N6" - or just 6 - (in this example)

It must be done with java standard stream's library, no use of third parties.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
Johnny Willer
  • 3,717
  • 3
  • 27
  • 51
  • Does it always start with N1 or is this unspecified? – Tunaki Mar 30 '16 at 17:46
  • @Tunaki is always "N something", something is always positive integer, but not always start with 1 – Johnny Willer Mar 30 '16 at 17:47
  • Why must it be done with streams? – Paul Boddington Mar 30 '16 at 17:48
  • @PaulBoddington curiosity purposes – Johnny Willer Mar 30 '16 at 17:48
  • 2
    Please don't try to use streams for things they're not intended for. – Louis Wasserman Mar 30 '16 at 17:52
  • 2
    @LouisWasserman so, do I need to surrender? :) – Johnny Willer Mar 30 '16 at 18:01
  • 3
    You can always compute the expected sum knowing the first and last element of the list, then compare this with the real sum of the elements. If it's the same, then the "missing" number is the next one in the sequence otherwise it's `expectedSum - sumInList`. But the stream part will be here used to only sum the elements of the list. – Alexis C. Mar 30 '16 at 18:23
  • your question gave me idea to Collections.sort(customerSet, Comparator.comparing(Customer::getId)); whenever new customer added and return customerSet.stream().map(Customer::getId).reduce(0,((integer, integer2) -> integer2==integer+1?integer2:integer))+1; for the new id – Prabhat Gaur May 06 '23 at 04:25

6 Answers6

10

The algorithm to implement here is based from this one: to find the missing number in a sequence of integers, the trick is to:

  • calculate the sum of the elements in the sequence.
  • calculate the sum of the elements the sequence would have with the missing number: this is easy to do since we can determine the minimum, the maximum and we know that the sum from a sequence of integer going from min to max is max*(max+1)/2 - (min-1)*min/2.
  • find the difference between those two sums: that's our missing number

In this case, we can collect statistics on our Stream by first mapping to an IntStream formed by only the numbers themselves and then calling summaryStatistics(). This returns a IntSummaryStatistics that has all the values we want: min, max and sum:

public static void main(String[] args) {
    List<String> arr = Arrays.asList("N3", "N7", "N4", "N5", "N2");
    IntSummaryStatistics statistics = 
        arr.stream()
           .mapToInt(s -> Integer.parseInt(s.substring(1)))
           .summaryStatistics();

    long max = statistics.getMax();
    long min = statistics.getMin();

    long missing = max*(max+1)/2 - (min-1)*min/2 - statistics.getSum();
    System.out.println(missing); // prints "6" here
}

If there is no missing number, this will print 0.

Community
  • 1
  • 1
Tunaki
  • 132,869
  • 46
  • 340
  • 423
  • Firstly, thank you so much for your time and effort, amazing answer. Will this work if there more than one number missing? example, `"N3", "N4", "N6", "N8"`, printing "N5" or "N7"? no need printing both. – Johnny Willer Mar 30 '16 at 18:13
  • 1
    @JohnnyWiller No, it will not. This assumes that there is a single missing number. Although it is possible to extend it to find two missing numbers. The logic is the same (refer to the linked question: it is based on the for the general case). – Tunaki Mar 30 '16 at 18:14
  • This is very clever, but I'm not sure that converting the stream to an `IntSummaryStatistics` objects and analysing the result really counts as using streams. You could just convert the stream into an `Iterator` and write a `for` loop. – Paul Boddington Mar 30 '16 at 18:24
  • Also, it requires iterating the entire List at least once (for calculating the sum), while a short-circuit soltuion would be possible if we are only interested in the first missing number. – Hulk Mar 31 '16 at 07:28
  • I’d rather use `(min+max)*(max-min+1)/2 - statistics.getSum()`… – Holger Mar 31 '16 at 10:55
  • 1
    @Paul Boddington: of course, you could just convert the stream into an `Iterator` and write a `for` loop, but that applies to all stream applications. What matters is that this is an operation composed of stateless operators, the small amount of post-processing arithmetic doesn’t count, but if it still bothers you, you can rewrite it to a “pure” stream solution: `long missing = arr.stream().collect( Collectors. collectingAndThen( Collectors.summarizingInt(s -> Integer.parseInt(s.substring(1))), s->{long max=s.getMax(),min=s.getMin(); return (min+max)*(max-min+1)/2-s.getSum();}));` – Holger Mar 31 '16 at 11:46
  • 2
    @Federico Peralta Schaffner: `max` is a `long` variable but the biggest value it can contain is `Integer.MAX_VALUE`, so no, this solution can not overflow. Btw. the term `min*(min-1)` can get even bigger than `max*(max+1)`, still, it fits into the `long` range without problems. – Holger Mar 31 '16 at 14:49
  • @Holger True, it's `IntSummaryStatistics`, so there's no possibility of overflow. – fps Mar 31 '16 at 15:10
5

Here's the solution involving the pairMap operation from my free StreamEx library. It prints all the missing elements of the sorted input:

ArrayList<String> arr = new ArrayList(Arrays.asList("N1", "N2", "N3", "N5"));
StreamEx.of(arr).map(n -> Integer.parseInt(n.substring(1)))
                .pairMap((a, b) -> IntStream.range(a+1, b))
                .flatMapToInt(Function.identity())
                .forEach(System.out::println);

The pairMap operation allows you to map every adjacent pair of the stream to something else. Here we map them to the streams of the skipped numbers, then flatten these streams.

The same solution is possible without third-party library, but looks more verbose:

ArrayList<String> arr = new ArrayList(Arrays.asList("N1", "N2", "N3", "N5"));
IntStream.range(0, arr.size()-1)
                .flatMap(idx -> IntStream.range(
                    Integer.parseInt(arr.get(idx).substring(1))+1,
                    Integer.parseInt(arr.get(idx+1).substring(1))))
                .forEach(System.out::println);
Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
  • Very useful, I have already saw some answers on SO based on your library :) thanks – Johnny Willer Mar 31 '16 at 11:16
  • Instead of `.mapToObj(…).flatMapToInt(Function.identity())` you can just use `.flatMap(…)` which makes the second solution much simpler, reducing the gap between the two solutions… – Holger Mar 31 '16 at 11:52
  • @Holger, thanks, here it works. I did not notice that I have ints at both ends of the stream (though they have completely different meaning). – Tagir Valeev Mar 31 '16 at 12:07
3

If there's only ONE missing number in the array, and if all numbers are positive, you could use the XOR algorithm, as explained in this question and its answers:

List<String> list = Arrays.asList("N5", "N2", "N3", "N6");
int xorArray = list.stream()
        .mapToInt(p -> Integer.parseInt(p.substring(1)))
        .reduce(0, (p1, p2) -> p1 ^ p2);
int xorAll = IntStream.rangeClosed(2, 6)
        .reduce(0, (p1, p2) -> p1 ^ p2);
System.out.println(xorArray ^ xorAll); // 4

The advantage of this approach is that you don't need to use extra data structures, all you need is a couple of ints.


EDIT as per @Holger's comments below:

This solution requires you to know the range of the numbers in advance. Although on the other hand, it doesn't require the list and stream to be sorted.

Even if the list wasn't sorted, you could still get min and max (hence, the range) with IntSummaryStatistics, but this would require an extra iteration.

Community
  • 1
  • 1
fps
  • 33,623
  • 8
  • 55
  • 110
  • @Holger I think you could get min and max and xor all the elements in a single pass by using a custom collector, with the finisher using min and max to xor all the range and finally xoring this whole range xor value with the previously computed xor value for the elements. – fps Mar 31 '16 at 16:49
  • You can do that but you said “the advantage of this approach is that you don't need to use extra data structures”, but a container holding [min, max, xor] isn’t so much different to an `IntSummaryStatistics` holding [min, max, sum]; the solutions are very similar as `+` and `^` aren’t so much different. Then the only difference is that your solution would also work with `LongStream`s as it is immune against overflows. – Holger Mar 31 '16 at 16:54
  • 1
    Will not serve for my situation but it's a very useful technique. Thanks for sharing. – Johnny Willer Mar 31 '16 at 17:18
2

You could create a state object which is used to transform a single input stream into multiple streams of missing entries. These missing entry streams can then be flat mapped to produce a single output:

public class GapCheck {
    private String last;

    public GapCheck(String first) {
        last = first;
    }

    public Stream<String> streamMissing(String next) {
        final int n = Integer.parseInt(next.replaceAll("N", ""));
        final int l = Integer.parseInt(last.replaceAll("N", ""));
        last = next;
        return IntStream.range(l + 1, n).mapToObj(Integer::toString);
    }
} 

Usage:

final List<String> arr = new ArrayList(Arrays.asList("N1", "N3", "N5"));

arr.stream()
   .flatMap(new GapCheck(arr.get(0))::streamMissing)
   .forEach(System.out::println);

output:

2
4
flakes
  • 21,558
  • 8
  • 41
  • 88
1

This is more work than you might expect, but it can be done with a collect call.

public class Main {
    public static void main(String[] args) {
        ArrayList<String> arr = new ArrayList<String>(Arrays.asList("N1", "N2", "N3", "N5", "N7", "N14"));

        Stream<Integer> st = arr.stream().map(p -> Integer.parseInt(p.substring(1))).sorted();
        Holder<Integer> holder = st.collect(() -> new Holder<Integer>(), 
                (h, i) -> {
                    Integer last = h.getProcessed().isEmpty() ? null : h.getProcessed().get(h.getProcessed().size() - 1);
                    if (last != null) {
                        while (i - last > 1) {
                            h.getMissing().add(++last);
                        }
                    }
                    h.getProcessed().add(i);
                }, 
                (h, h2) -> {});
        holder.getMissing().forEach(System.out::println);
    }

    private static class Holder<T> {
        private ArrayList<T> processed;
        private ArrayList<T> missing;

        public Holder() {
            this.processed = new ArrayList<>();
            this.missing = new ArrayList<>();
        }

        public ArrayList<T> getProcessed() {
            return this.processed;
        }

        public ArrayList<T> getMissing() {
            return this.missing;
        }
    }
}

This prints

4
6
8
9
10
11
12
13

Note that this sort of thing isn't really a particularly strong fit for Streams. All of the stream processing methods will tend to pass you each item exactly one time, so you need to handle all runs of missing numbers at once, and in the end, you're writing kind of a lot of code to avoid just writing a loop.

Ian McLaird
  • 5,507
  • 2
  • 22
  • 31
  • You should not provide a broken function like `(h, h2) -> {}` as combiner. It isn’t too hard to provide a correct function here, all you need to do is to perform two `addAll` invocations… – Holger Mar 31 '16 at 15:16
1

Here is one solution using pure streams, albeit not very efficient.

public void test() {
    List<String> arr = new ArrayList(
                    Arrays.asList("N1", "N2", "N3", "N5", "N7"));

    List<Integer> list = IntStream
                .range(1, arr.size())
                .mapToObj(t -> new AbstractMap.SimpleEntry<Integer, Integer>(
                        extract(arr, t), extract(arr, t) - extract(arr, t - 1)))
                .filter(t -> t.getValue() > 1)
                .map(t -> t.getKey() - 1)
                .collect(Collectors.toList());

    System.out.println(list);
}

private int extract(List<String> arr, int t) {
    return Integer.parseInt(arr.get(t).substring(1));
}

Major performance block will be because of repeated parsing of list elements. However, this solution will be able to provide all missing numbers.

Mrinal
  • 1,846
  • 14
  • 17