Zombies
Zombies

Reputation: 25902

Create a stream of custom alternating numbers

How can I make an IntStream that starts in the middle of a given sequential range, and then streams the next numbers starting in the middle and alternating to the left and right. For example, for a given sequential range of numbers 1 2 3 4 5, the custom sequence would be 3 2 4 1 5 or 3 4 2 5 1 depending whether you start with left or right first.

I basically am trying to iterate through an array starting from the middle and going outward evenly (not going to the left or right fully first).

I have tried this using just for loops but the code is messy and I think it would be much better to just line up a collection or stream of numbers instead of checking for it on the fly (because of all the index out of bounds exceptions that have to be checked for). Here is the original code that I think would be much better as a pre computed stream of ints:

        int middle = myArray.length / 2;
        Object value = myArray[middle]; //have to reference middle since the for loop won't
        //do operation on value
        for (int offset = 1; true; offset++) {
            int nextRight = middle + offset;
            int nextLeft = middle - offset;
            if (nextRight < myArray.length) { // Have to guard against exception, but can't catch exception ahead of time because left or null may not be empty.
                Object value = myArray[nextRight];
                //do operation on value
            }
            if (nextLeft >= 0) {
                Object value = myArray[nextRight];
                //do operation on value
            }
            if (nextRight >= myArray.length) {
                break; //exit logic
            }
            if (nextLeft < 0) {
                break; //exit logic
            }
        }

Upvotes: 12

Views: 1274

Answers (7)

Adrian
Adrian

Reputation: 31

Used formula to generate stream for any sequential range from start to end

public static IntStream shuffle(int start, int end){
    int size = end - start + 1;
    int center = start + ((size + 1) >> 1) - 1;
    int even = (size + 1) & 1;
    int direction = 1 - (even << 1); //for left first: (even << 1) - 1;
    return IntStream.range(1, size + 1)
            .map(i -> center + (even + i * direction * ((((i + 1) & 1) << 1) - 1)) / 2);
}

Upvotes: 1

Holger
Holger

Reputation: 298439

Since you said, you want to use the sequence to iterate over an array, I changed it to produce numbers including zero, but excluding n, so you can directly pass in an array length and get valid indices.

Then you can use

static IntStream altSeq(int n) {
    int mid = (n-1)/2;
    return IntStream.rangeClosed(1, n)
                    .map(i -> mid + (i>>>1)*signum(rotateRight(i,1)));
}

The approach is similar to Stuart Marks’ answer, but uses an IntStream.rangeClosed() as base, which creates a sized stream, which works much more efficient than creating an infinite stream (like iterate) and applying a limit, especially for operations like toArray, count, and for parallel streams. In other words, the performance is on par with iterating over the range/array in the usual order.

The natural number range is converted by using the lowest bit as sign, which is alternating for ascending numbers, and shifting the numbers one bit to the right, which is equivalent to dividing the magnitude of the numbers by two.

An alternative notation performing only one bitshift per element would be

static IntStream altSeq(int n) {
    int mid = (n-1)/2;
    return IntStream.rangeClosed(1, n)
                    .map(i -> Integer.rotateRight(i, 1))
                    .map(i -> mid + (i&Integer.MAX_VALUE)*signum(i));
}

Upvotes: 2

Stuart Marks
Stuart Marks

Reputation: 132460

Try this:

import static java.lang.Integer.signum;

static IntStream altSeq(int n) {
    return IntStream.iterate(2 * (n % 2) - 1, i -> -i - signum(i))
                    .map(i -> i / 2 + (n + 1) / 2)
                    .limit(n);
}

For example, altSeq(5) produces:

[3, 2, 4, 1, 5]

and running altSeq(6) produces:

[3, 4, 2, 5, 1, 6]

Briefly, we generate an ascending sequence that alternates sign:

1, -2, 3, -4, 5, ...

That's what the i -> -i - signum(i) expression does. Then, we divide by two in order to get the offsets from the midpoint:

0, -1, 1, -2, 2, ...

That's where the i / 2 in the first term of the map expression comes from. Then we add the midpoint of the range 1..n which is (n + 1) / 2, the second term of the map expression. For n = 5, this gives us

3, 2, 4, 1, 5, ...

Starting at 1 works for odd length sequences. For even lengths, we want to start with -1. The seed expression 2 * (n % 2) - 1 computes the right seed value for both even and odd length sequences.

Finally, we apply limit(n) to terminate the sequence.

Upvotes: 8

David Soroko
David Soroko

Reputation: 9096

As you are willing to consider a collection, the following "low tech" approach is quite straightforward:

public static List<Integer> f(int len) {
    int offset = len / 2;

    ArrayList<Integer> indices = new ArrayList<>(len);

    for(int i = 0 ; i < len; i++) {
        int index = offset + i * direction(i);
        indices.add(index);
        offset = index;
    }

    return indices;
}


private static int direction(int size) {
    return (size & 1) == 0 ? 1 : -1;
}

The call to f(5), returns: [2, 1, 3, 0, 4]

The direction ( left to right or right to left ) can be be modified by changing direction(). If you really need 1-based indices, modify f() to have: indices.add(index+1)

Upvotes: 1

Eugene
Eugene

Reputation: 120978

Well, I can think of this, don't know if it fits your requirements though:

public static IntStream generate(int[] x, boolean direction) {
    int length = x.length / 2;

    IntStream right = IntStream.range(1, length + 1)
            .mapToObj(i -> {
                return direction ? new Integer[] { i, -1 * i } : new Integer[] { -1 * i, i };
            })
            .flatMap(Arrays::stream)
            .mapToInt(i -> x[length + i]);

    return IntStream.concat(IntStream.of(x[length]), right);
}

Upvotes: 2

PaperMonoid
PaperMonoid

Reputation: 94

This solution uses iterators and streams:

boolean toLeft = false;
int size = 5;

int half = size % 2 == 0 ? size / 2 : size / 2 + 1;
IntStream inferiorStream = IntStream.iterate (half, x -> x - 1);
IntStream superiorStream = IntStream.iterate (half, x -> x + 1);

OfInt a = toLeft 
          ? inferiorStream.iterator () 
          : superiorStream.iterator ();
OfInt b = toLeft 
          ? superiorStream.skip (1).iterator () 
          : inferiorStream.skip (1).iterator ();

IntStream stream = Stream.generate (() -> IntStream.concat (
    a.hasNext () ? IntStream.of (a.nextInt ()) : IntStream.empty (),
    b.hasNext () ? IntStream.of (b.nextInt ()) : IntStream.empty ()))
    .flatMapToInt (Function.identity ())
    .limit (size);

stream.forEach (System.out :: println);

Output (toLeft = true):

3
4
2
5
1

Output (toLeft = false):

3
2
4
1
5

Upvotes: 2

Saravana
Saravana

Reputation: 12817

we can customise the IntStream.generate method to generate the IntStream in the sequence we need

public void run(String... args) throws Exception {
    final int[] arr = IntStream.range(0, 9).toArray(); //test data
    int mid = arr.length / 2;
    SeqGen seq = new SeqGen(arr, mid);
    List<Integer> ints = IntStream.generate(() -> seq.gen())
            .limit(arr.length)
            .boxed()
            .collect(Collectors.toList());
    System.out.println(ints);
}

SeqGen

private class SeqGen {
    int[] arr;
    int start;
    int curr;
    boolean flag;

    public SeqGen(int[] arr, int start) {
        this.arr = arr;
        this.start = start;
    }

    public int gen() {
        int ret = -1;
        int l = arr[start + curr];
        int r = arr[arr.length - curr - start - 1];
        if (!flag) {
            ret = l;
            curr++;
        } else {
            ret = r;
        }
        flag = !flag;
        return ret;
    }
}

output

[4, 3, 5, 2, 6, 1, 7, 0, 8]

Upvotes: 1

Related Questions