Как реализовать Spliterator для потоковой передачи чисел Фибоначчи?

Я играю с Java 8 Spliterator и создал один для потоковой передачи чисел Фибоначчи до заданного n. Так что для серии Фибоначчи 0, 1, 1, 2, 3, 5, 8, ...

n    fib(n)
-----------
-1   0
1    0
2    1
3    1
4    2

Ниже приведена моя реализация, которая печатает кучу 1, прежде чем закончится память стека. Можете ли вы помочь мне найти ошибку? (Я думаю, что это не продвигает currentIndex но я не уверен, какое значение это установить).

Изменить 1: Если вы решите ответить, пожалуйста, оставьте его релевантным для вопроса. Этот вопрос не об эффективной генерации числа Фибоначчи; речь идет об обучении сплитераторов.

FibonacciSpliterator:

@RequiredArgsConstructor
public class FibonacciSpliterator implements Spliterator<FibonacciPair> {
    private int currentIndex = 3;
    private FibonacciPair pair = new FibonacciPair(0, 1);

    private final int n;

    @Override
    public boolean tryAdvance(Consumer<? super FibonacciPair> action) {
//        System.out.println("tryAdvance called.");
//        System.out.printf("tryAdvance: currentIndex = %d, n = %d, pair = %s.\n", currentIndex, n, pair);

        action.accept(pair);

        return n - currentIndex >= 2;
    }

    @Override
    public Spliterator<FibonacciPair> trySplit() {
//        System.out.println("trySplit called.");

        FibonacciSpliterator fibonacciSpliterator = null;

        if (n - currentIndex >= 2) {
//            System.out.printf("trySplit Begin: currentIndex = %d, n = %d, pair = %s.\n", currentIndex, n, pair);

            fibonacciSpliterator = new FibonacciSpliterator(n);

            long currentFib = pair.getMinusTwo() + pair.getMinusOne();
            long nextFib = pair.getMinusOne() + currentFib;

            fibonacciSpliterator.pair = new FibonacciPair(currentFib, nextFib);
            fibonacciSpliterator.currentIndex = currentIndex + 3;

//            System.out.printf("trySplit End: currentIndex = %d, n = %d, pair = %s.\n", currentIndex, n, pair);
        }

        return fibonacciSpliterator;
    }

    @Override
    public long estimateSize() {
        return n - currentIndex;
    }

    @Override
    public int characteristics() {
        return ORDERED | IMMUTABLE | NONNULL;
    }
}

FibonacciPair:

@RequiredArgsConstructor
@Value
public class FibonacciPair {
    private final long minusOne;
    private final long minusTwo;

    @Override
    public String toString() {
        return String.format("%d %d ", minusOne, minusTwo);
    }
}

Использование:

Spliterator<FibonacciPair> spliterator = new FibonacciSpliterator(5);

StreamSupport.stream(spliterator, true)
    .forEachOrdered(System.out::print);

3 ответа

Решение

Хорошо, давайте напишем сплитератор. С помощью OfLong все еще слишком скучно: давайте перейдем к BigInteger и не ограничивайте пользователя 92. Хитрость здесь в том, чтобы быстро перейти к заданному числу Фибоначчи. Я буду использовать алгоритм умножения матриц, описанный здесь для этой цели. Вот мой код:

static class FiboSpliterator implements Spliterator<BigInteger> {
    private final static BigInteger[] STARTING_MATRIX = {
        BigInteger.ONE, BigInteger.ONE, 
        BigInteger.ONE, BigInteger.ZERO};

    private BigInteger[] state; // previous and current numbers
    private int cur; // position
    private final int fence; // max number to cover by this spliterator

    public FiboSpliterator(int max) {
        this(0, max);
    }

    // State is not initialized until traversal
    private FiboSpliterator(int cur, int fence) {
        assert fence >= 0;
        this.cur = cur;
        this.fence = fence;
    }

    // Multiplication of 2x2 matrix, by definition      
    static BigInteger[] multiply(BigInteger[] m1, BigInteger[] m2) {
        return new BigInteger[] {
            m1[0].multiply(m2[0]).add(m1[1].multiply(m2[2])),
            m1[0].multiply(m2[1]).add(m1[1].multiply(m2[3])),
            m1[2].multiply(m2[0]).add(m1[3].multiply(m2[2])),
            m1[2].multiply(m2[1]).add(m1[3].multiply(m2[3]))};
    }

    // Log(n) algorithm to raise 2x2 matrix to n-th power       
    static BigInteger[] power(BigInteger[] m, int n) {
        assert n > 0;
        if(n == 1) {
            return m;
        }
        if(n % 2 == 0) {
            BigInteger[] root = power(m, n/2);
            return multiply(root, root);
        } else {
            return multiply(power(m, n-1), m);
        }
    }

    @Override
    public boolean tryAdvance(Consumer<? super BigInteger> action) {
        if(cur == fence)
            return false; // traversal finished
        if(state == null) {
            // initialize state: done only once
            if(cur == 0) {
                state = new BigInteger[] {BigInteger.ZERO, BigInteger.ONE};
            } else {
                BigInteger[] res = power(STARTING_MATRIX, cur);
                state = new BigInteger[] {res[1], res[0]};
            }
        }
        action.accept(state[1]);
        // update state
        if(++cur < fence) {
            BigInteger next = state[0].add(state[1]);
            state[0] = state[1];
            state[1] = next;
        }
        return true;
    }

    @Override
    public Spliterator<BigInteger> trySplit() {
        if(fence - cur < 2)
            return null;
        int mid = (fence+cur) >>> 1;
        if(mid - cur < 100) {
            // resulting interval is too small:
            // instead of jumping we just store prefix into array
            // and return ArraySpliterator
            BigInteger[] array = new BigInteger[mid-cur];
            for(int i=0; i<array.length; i++) {
                tryAdvance(f -> {});
                array[i] = state[0];
            }
            return Spliterators.spliterator(array, ORDERED | NONNULL | SORTED);
        }
        // Jump to another position
        return new FiboSpliterator(cur, cur = mid);
    }

    @Override
    public long estimateSize() {
        return fence - cur;
    }

    @Override
    public int characteristics() {
        return ORDERED | IMMUTABLE | SIZED| SUBSIZED | NONNULL | SORTED;
    }

    @Override
    public Comparator<? super BigInteger> getComparator() {
        return null; // natural order
    }
}

Эта реализация на самом деле быстрее параллельно для очень больших fence значение (как 100000). Возможно, возможна даже более мудрая реализация, которая бы неравномерно использовала промежуточные результаты умножения матриц.

Помимо того факта, что ваш код неполон, есть как минимум две ошибки в вашем tryAdvance Метод узнаваем. Во-первых, вы на самом деле не делаете каких-либо авансов. Вы не изменяете состояние вашего сплитератора. Во-вторых, вы безоговорочно ссылаетесь на действие accept метод, который не соответствует тому факту, что вы возвращаете условное значение, а не true,

Цель tryAdvance является:

  • как следует из названия, попробуйте сделать заранее, то есть рассчитать следующее значение
  • если есть следующее значение, вызвать action.accept с этим значением и возвратом true
  • в противном случае просто вернитесь false

Обратите внимание, что ваш trySplit() выглядит не очень убедительно, я даже не знаю с чего начать. Вы лучше, наследуя от AbstractSpliterator и не реализуя обычай trySplit(), Ваша операция все равно не выиграет от параллельного выполнения. Поток, созданный из этого источника, может получить преимущество от параллельного выполнения, только если вы объедините его в цепочку тихими дорогими операциями для каждого элемента.

В общем случае вам не нужно реализовывать сплитератор. Если вам действительно нужно Spliterator объект, вы можете использовать поток для этой цели:

Spliterator.OfLong spliterator = Stream
        .iterate(new long[] { 0, 1 },
                prev -> new long[] { prev[1], prev[0] + prev[1] })
        .mapToLong(pair -> pair[1]).spliterator();

Тестирование:

for(int i=0; i<20; i++)
    spliterator.tryAdvance((LongConsumer)System.out::println);

Обратите внимание, что проведение чисел Фибоначчи в long Переменная сомнительна: она переполняется после числа Фибоначчи 92. Поэтому, если вы хотите создать сплитератор, который просто перебирает первые 92 числа Фибоначчи, я бы предложил использовать предопределенный массив для этой цели:

Spliterator.OfLong spliterator = Spliterators.spliterator(new long[] {
        1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765,
        10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309,
        3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141,
        267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073L, 4807526976L,
        7778742049L, 12586269025L, 20365011074L, 32951280099L, 53316291173L, 86267571272L, 139583862445L,
        225851433717L, 365435296162L, 591286729879L, 956722026041L, 1548008755920L, 2504730781961L,
        4052739537881L, 6557470319842L, 10610209857723L, 17167680177565L, 27777890035288L,
        44945570212853L, 72723460248141L, 117669030460994L, 190392490709135L, 308061521170129L,
        498454011879264L, 806515533049393L, 1304969544928657L, 2111485077978050L, 3416454622906707L,
        5527939700884757L, 8944394323791464L, 14472334024676221L, 23416728348467685L, 37889062373143906L,
        61305790721611591L, 99194853094755497L, 160500643816367088L, 259695496911122585L, 420196140727489673L,
        679891637638612258L, 1100087778366101931L, 1779979416004714189L, 2880067194370816120L,
        4660046610375530309L, 7540113804746346429L
}, Spliterator.ORDERED);

Массив-сплитератор также хорошо разбивается, поэтому у вас будет реальная параллельная обработка.

Другие вопросы по тегам