3

As an exercise to learn Java 8+ Streams I wanted to cast some of my simple Codility implementations as a Stream solution.

For example, the BinaryGap problem .. a simple one liner solution using Streams could be something like:

public static int solution(int N) {
    return Integer.toBinaryString(N).chars().
                     filter(x -> x == '1').whichIndexes().diff().max();
}

Only problem though, whichIndexes and diff don't exist. I need a way to get hold of the index of the filtered elements and then compute their pair-wise differences and that'd be a good starting point for a Streams' based one-liner solution.

UPDATE: here is my BinaryGap solution in C++, the Java non-Stream-ed version would be very similar though:

#include <bitset>
#include <iostream>
#include <math.h>

using namespace std;

int solution(int N) {
    bitset<32> bs(N);
    int maxGap = 0;
    std::size_t i = 0;
    while (bs[i] == 0) {
        i++;
    }
    int startPos = i;
    for (; i < bs.size(); ++i) {
        if (bs[i] == 1) {
            int gap = i - startPos - 1;
            if (gap > maxGap) {
                maxGap = gap;
            }
            startPos = i;
        }
    }
    return maxGap;
}

int main() {
    cout << solution(9) << " must be 2" << endl;
    cout << solution(529) << " must be 4" << endl;
    cout << solution(20) << " must be 1" << endl;
    cout << solution(32) << " must be 0" << endl;
    cout << solution(1041) << " must be 5" << endl;
    return 0;
}
SkyWalker
  • 13,729
  • 18
  • 91
  • 187
  • Regarding *already has answers*: meh, but together with [this post](https://stackoverflow.com/questions/20470010/collect-successive-pairs-from-a-stream/20473797#20473797) its covered more or less. – Curiosa Globunznik Nov 30 '19 at 19:29

4 Answers4

6

Getting the indices is straightforward; stream a range of integers instead of the characters from the string.

String s = Integer.toBinaryString(N);
IntStream.range(0, s.length())
    .filter(i -> s.charAt(i) == '1')
    . // more stream operations

Computing the maximum difference is harder, since this depends on consecutive pairs from the stream, not individual elements. Anything which depends on multiple elements from the stream (rather than one at a time) has to be done in the collect stage. This is a bit tricky to get right: the IntStream.collect method takes three arguments:

  • A Supplier<R> which supplies a new object of type R to collect results in,
  • An ObjIntConsumer<R> which accumulates an object of type R with one more int,
  • A BiConsumer<R, R> which doesn't do anything in a non-parallel stream.

The type R will need to keep track of the the current maximum difference, and the last number seen so that the difference with the next number can be computed. One way to do this is to have a List of two numbers, where index 0 holds the max difference so far, and index 1 holds the previous number seen (if any).

String s = Integer.toBinaryString(N);
int maxDiff = IntStream.range(0, s.length())
    .filter(i -> s.charAt(i) == '1')
    .collect(
        // supply a new list to hold intermediate results
        () -> {
            List<Integer> acc = new ArrayList<Integer>();
            acc.add(0); // initial max diff; and result if no "gap" exists
            return acc;
        },
        // accumulate one more number into the result
        (List<Integer> list, int num) -> {
            if(list.size() == 1) {
                // this is the first number, no diffs yet
                list.add(num);
            } else {
                int max = list.get(0);
                int lastNum = list.get(1);
                int diff = num - lastNum;
                list.set(0, diff > max ? diff : max);
                list.set(1, num);
            }
        },
        // combine two accummulators; shouldn't be called
        (List<Integer> list1, List<Integer> list2) -> {
            throw new RuntimeException("combiner shouldn't be called");
        }
    )
    .get(0); // max diff is at index 0 in the list

This is almost certainly a worse solution than what you had before trying to use streams, but... well, you wanted a stream solution, so here it is.

kaya3
  • 47,440
  • 4
  • 68
  • 97
  • My solution computes the maximum difference between the indices, which is what was asked for in the question. The binary sequence `10001` has 1s at indices 0 and 4, so the difference is 4. – kaya3 Nov 29 '19 at 20:55
  • the problem _itself_ is different though as states in that link – Eugene Nov 29 '19 at 20:56
  • I am answering a question on Stack Overflow, not solving a problem on Codility. Your objection seems to be that the question is wrong. – kaya3 Nov 29 '19 at 20:56
  • _and then compute the pair-wise differences_, well that to me means a different thing that you showed. – Eugene Nov 29 '19 at 21:18
  • Pairwise differences means the differences between each pair. What else could it possibly mean? – kaya3 Nov 29 '19 at 21:24
  • @kaya3 understandable. I was mainly looking into the main problem from the link, anyway the diff is small even if you take that into consideration or not. The part that makes me sad (even if I tried hard) is that there is no way to merge two combiners in a parallel stream. pity. On the other hand, you _could_ use an array for simplicity, though not required (see next comment). otherwise, 1+ – Eugene Nov 30 '19 at 05:08
  • `() -> new int[]{-1, 0}, (arr, num) -> {if (arr[0] == -1) { arr[0] = 0; } else { int max = arr[0]; int lastNum = arr[1]; int diff = num - lastNum - 1; arr[0] = Math.max(diff, max); } arr[1] = num; }` – Eugene Nov 30 '19 at 05:08
  • Yes, an array is a little simpler. Regarding a combiner for parallel streams, this is possible if you store three numbers rather than two: first number in the sequence, max diff so far, and last number. To combine, find the diff between list1's last number and list2's first number, then the max diff is either that, or list1's max diff, or list2's maxdiff, whichever is largest. – kaya3 Nov 30 '19 at 21:07
  • this is exactly what I though of, but can you consume two elements at the same time from a stream? And you need to - to be able to pull this off. And while you could, this would complicate things quite a a lot. – Eugene Dec 01 '19 at 03:43
  • Not sure what you mean; each accumulator consumes one element at a time, and the combiner merges two accumulators. As far as I can tell, the combiner will only be called on adjacent accumulators (since the operation is not assumed to be commutative), so the last number seen by the first accumulator will be the one immediately preceding the first number seen by the second accumulator. – kaya3 Dec 01 '19 at 15:50
5

You can't get at the indexes of the elements in one single stream unless you feed indices from the beginning, like the IntStream trick does. If you feed it Strings only, you're lost.

Zipping the streamed characters with another stream of Integers to operate eventually on a Pair<String, Integer> would be another way to introduce indexes, but there's no built in zip method in the streaming framework (yet). There are libraries and workarounds (see link).

Also you can't operate on consecutive elements, all you ever see (outside the collect stage or the reduce method, which is of no help for the purpose) is the element at hand (except you come up with dirty tricks as shown below and).

Btw. here's a potential one-liner that popped up in the process, perhaps with some edge-case flaws regarding binary gap (ask @Holger (or Eugene), I think, they're still discussing...). I'm not familiar with its fine print actually, and it is not relevant for the OP (IMO).

Arrays.stream(Integer.toBinaryString(n).replaceAll("0*$|^0*", "").split("1"))
      .map(String::length)
      .reduce(0, Math::max)

This one I think is as close as you can get (without zipping) to your initial idea, with elements scavenged from @Kaya3 and this interesting hack to get pairs from consecutive elements ({1,2,3,4} -> {{1,2},{2,3},{3,4}}). You could do the diff in the map-to-pair step too, spares another line maybe, but its weird enough like this:

        String st = Integer.toBinaryString(N);
        Integer max = IntStream.range(0, st.length() - 1)
                .filter(i -> st.charAt(i) == '1')
                .boxed()
                .map(new Function<Integer, Pair>() {
                    Integer previous;

                    @Override
                    public Pair apply(Integer i) {
                        Pair p = new Pair(previous, i);
                        previous = i;
                        return p;
                    }
                }).skip(1) //first element is {null, smthg}
                .map(i -> (i.right - i.left) - 1)
                .max(Integer::compareTo)
                .orElse(0);

But mind you: "This [map hack] is completely contrary to the design of the streams framework and directly violates the contract of the map API, as the anonymous function is not stateless. Try running this with a parallel stream and ... you will see ... infrequent random "errors" almost impossible to reproduce ... This can be disastrous."

I think, used with parallelStream() it would give you results that are even guaranteed to be wrong, because then as many pairs are missing as there are partitions of the stream.

Curiosa Globunznik
  • 3,129
  • 1
  • 16
  • 24
  • Nice idea, your one liner. Not very efficient, but quiet compact. But a few points here. `toBinaryString` does not produce leading zeros, but there might be more than one trailing zero, so you need a `*`. In other words, it should be `.replaceAll("0*$", "")`. Also, this solution can’t distinguish between a maximum gap size of zero and a number not having any gap at all. (e.g. `0` or `0b0100` should be considered as having no gap, whereas `0b00110` or `0b001100` each have a zero size gap). – Holger Nov 29 '19 at 17:00
  • @Holger but does it really matter if we treat no gap as a zero gap or vice-versa? the function should return an `int`, not an `OptionalInt` for example where one could distinguish between "empty" and a "zero gap". Also why `*` and not `+` in the `replaceAll` part? don't you want to replace "all zeroes at the end"? Gurioso 1+ for the solution you have removed, which IMHO you should have not. – Eugene Nov 30 '19 at 05:39
  • I don't want to interfere in your and Holger's family business really. But since the version attracts attention I'll restore it. – Curiosa Globunznik Nov 30 '19 at 06:38
  • @gurioso that version isn’t bad; I never wanted you to remove it. It’s just worth naming the drawbacks of a solution (I also mentioned the readability issue of mine). So I’m happy that you decided to re-include it. – Holger Dec 02 '19 at 08:51
  • @Eugene it’s a matter of task definition. The OP will decide for a solution, however, for future readers, I’d mention the semantic difference between “no gap” and “zero gap”. It depends on the, cough, practical use case of this operation, whether the difference matters or not. – Holger Dec 02 '19 at 08:54
  • @Holger I just was to lazy to think about it again, thought it was irrelevant for the OP. Was just there to show off ;) And as far as the OP is concerned I think its even off-topic, but I was the only one with this view. Nah, the OP commented later, that it wasn't meant as a binary gap competition, but who cares now... – Curiosa Globunznik Dec 02 '19 at 14:29
3

Note that a computer does already compute in the binary system whereas both, converting a value to a String and processing a String, are expensive operations, compared to processing an int value.

To stream over the bits of an int, you can use the following solutions:

Given

int value = 1001;
IntStream.range(0, 32)
    .filter(i -> (value & (1 << i)) != 0)
    .forEach(System.out::println);

This utilizes the fact that we know that there can be at most 32 bits and it just checks for each whether it is set in the int value. The printing action is just for demonstration.

Alternatively, we can use a builtin tool for bit manipulations:

BitSet.valueOf(new long[] { Integer.toUnsignedLong(value) }).stream()
    .forEach(System.out::println);

For completeness, an iteration logic like with a loop

IntStream.iterate(value, i -> i != 0, i -> i - Integer.highestOneBit(i))
    .map(i -> Integer.numberOfTrailingZeros(Integer.highestOneBit(i)))
    .forEach(System.out::println);

But this iterate method with an end condition requires Java 9. For Java 8, we would need a work-around:

IntStream.iterate(value, i -> i - Integer.highestOneBit(i))
    .limit(32).filter(i -> i != 0)
    .map(i -> Integer.numberOfTrailingZeros(Integer.highestOneBit(i)))
    .forEach(System.out::println);

This again uses the fact that we know the maximum number of bits, further that we know that after encountering a zero, no non-zero value will appear, so filter is semantically equivalent to a stop condition here.

Still, a Stream is not a good tool for the tasks of processing two adjacent elements. As the other answers show, a Stream solution is complicated and still, these solutions are not fully adhering to the contract of the Stream API, bearing certain limitations, you have to keep in mind.

In contrast, a BitSet based loop solution looks like:

BitSet bs = BitSet.valueOf(new long[] { Integer.toUnsignedLong(value) });
int maxDiff = 0;
for(int s = bs.nextSetBit(0), e; (e = bs.nextSetBit(s + 1)) >= 0; s = e) {
    maxDiff = Math.max(maxDiff, e - s);
}
System.out.println(maxDiff == 0? "No range at all":
    "max diff: "+maxDiff+", resp, max gap "+(maxDiff-1));

Note that the gap, i.e the number of zeros between two one bits, is one smaller than the difference between the bit numbers.

We can perform the operation entirely with intrinsic int operations, which will be the most efficient:

int maxDiff = 0;
for(int i = value, bit1 = Integer.lowestOneBit(i), bit2,
        s = Integer.numberOfTrailingZeros(bit1), e;
        (bit2 = Integer.lowestOneBit(i -= bit1)) != 0; bit1 = bit2, s = e) {
    e = Integer.numberOfTrailingZeros(bit2);
    maxDiff = Math.max(maxDiff, e - s);
}
System.out.println(maxDiff == 0? "No range at all":
    "max diff: "+maxDiff+", resp, max gap "+(maxDiff-1));

This is a bit hard to read, but still way simpler than the Stream solution…

Holger
  • 285,553
  • 42
  • 434
  • 765
0

My first intuition would be to go for a regex, something like this:

static int regex(int n) {
    String s = Integer.toBinaryString(n);
    Pattern p = Pattern.compile("1(0+)(?=1)");
    Matcher m = p.matcher(s);
    int max = 0;
    while (m.find()) {
        max = Math.max(max, m.end(1) - m.start(1));
    }
    return max;
}

If you have java-9, this could be written in "one-liner" too:

static int regexMineJava9(int n) {
    return Pattern.compile("1(0+)(?=1)")
                  .matcher(Integer.toBinaryString(n))
                  .results()
                  .mapToInt(mr -> mr.end(1) - mr.start(1))
                  .max()
                  .orElse(0);
}

To me this is a bit more readable than this for example:

static int loop(int n) {
    int max = 0;

    while (n != 0) {
        int left = Integer.numberOfTrailingZeros(Integer.highestOneBit(n));
        n = n & ~(1 << left);
        int right = Integer.numberOfTrailingZeros(Integer.highestOneBit(n));

        if (right != 32) {
            max = left - right - 1;
        }
    }

    return max;
}

Though the last loop should be the most performant one.

For the fun of it here is a BitSet also, pretty much what Holger did, but a bit un-winded:

static int bitSet(int n) {
    BitSet bitSet = BitSet.valueOf(new long[]{Integer.toUnsignedLong(n)});
    int left = bitSet.nextSetBit(0);
    int max = 0;
    while (left != -1) {
        int right = bitSet.nextSetBit(left + 1);
        if (right == -1) {
            break;
        }
        max = Math.max(right - left - 1, max);
        left = right;
    }

    return max;
}
Eugene
  • 117,005
  • 15
  • 201
  • 306