【问题标题】:Is it possible to create in Java 8 a unlimitedly growing in lazy way collection, defined by recursion?是否可以在 Java 8 中创建由递归定义的无限增长的惰性集合?
【发布时间】:2016-02-22 22:02:48
【问题描述】:

我可以创建一个递归闭包:

static IntUnaryOperator fibo;
fibo = 
    (i) -> 
    i<2 ? 1 : fibo.applyAsInt(i-1)+ fibo.applyAsInt(i-2);

当然,它只是作为一个例子才有意义。为了有用,这样的集合应该保留已经计算过的元素并获取()它们而不重新计算。最初需要时,元素的计数应该以惰性的方式进行。因此,没有成员将不得不计算超过一次。通过这种方式,我们将得到一个看起来像递归定义的序列的结构,并且快速且可重用。

当我开始研究 Java 8 时,我认为 Stream 就是这样工作的。但它没有,因为流不能被使用两次。

我想到了以下构造:

IntStream fi;
fi=IntStream.iterate(0, i -> fi[i-1]+fi[i-2]);

但是那样它就行不通了——我不能通过索引从流中获取一个项目。另一个问题是,如果我以后沿着流走,它会被消耗掉,我不能使用它反复。如果我将流复制到List,它就不再懒惰了。

因此,我需要一些可以按索引处理的结构。如fibo(i)

编辑。显然,解决方案不能是流,因为流不能被使用两次。我不想在每次调用 F(i) 时重复所有计算。

【问题讨论】:

  • 你可能想创建一个Spliterator.OfInt
  • @fge 我不需要顺序访问。通过生成数组可以更轻松地完成。
  • 数组不是惰性的,也不能是无限的;结果我真的不确定你在问什么
  • @fge 注意问题的标题 - 懒惰和迭代。如果您使用顺序访问,它不是迭代的...... Java 数组不是惰性的,但我可以通过仅在第一次使用时才会填充的引用来填充它。因此它变得懒惰。很简单,就是太长了。并且对 Java 8 没有用处。

标签: java recursion java-8 closures java-stream


【解决方案1】:

该解决方案将创建为一个类FunctionalSequence,用于表示由具有整数参数的 lambda 函数定义的惰性无限对象序列。该函数可以是迭代的,也可以不是。对于迭代情况,FunctionalSequence 类将有一个方法 initialize 用于设置起始值。

此类对象的声明如下所示:

    FunctionalSequence<BigInteger> fiboSequence =  new FunctionalSequence<>();
    fiboSequence.
        initialize(Stream.of(BigInteger.ONE,BigInteger.ONE)).
        setSequenceFunction(
            (i) ->
            fiboSequence.get(i-2).add(fiboSequence.get(i-1))
        );

请注意,就像问题中的递归 lambda 示例一样,我们不能声明对象并在一个运算符中递归地定义它。一个运算符用于声明,另一个用于定义。

FunctionalSequence 类定义:

import java.util.Iterator;
import java.util.LinkedList;
import java.util.stream.Stream;

public class FunctionalSequence<T> implements Iterable<T>{
    LinkedList<CountedFlighweight<T>> realList = new LinkedList<>();
    StackOverflowingFunction<Integer, T> calculate = null;
    public FunctionalSequence<T> initialize(Stream<T> start){
        start.forEachOrdered((T value) ->
        {
                realList.add(new CountedFlighweight<>());
                realList.getLast().set(value);
        });
        return this;
    }
    public FunctionalSequence<T>  setSequenceFunction(StackOverflowingFunction<Integer, T> calculate){
        this.calculate = calculate;
        return this;
    }

    @Override
    public Iterator<T> iterator() {
        return new SequenceIterator();
    }
    public T get(int currentIndex) throws StackOverflowError{
        if(currentIndex < 0) return null;
        while (currentIndex >= realList.size()){
            realList.add(new CountedFlighweight<T>());
        }
        try {
            return (T) realList.get(currentIndex).get(calculate, currentIndex);
        } catch (Exception e) {
            return null;
        }
    }
    public class SequenceIterator implements Iterator<T>{
        int currentIndex;
        @Override
        public boolean hasNext() {
            return true;
        }

        @Override
        public T next() {
            T result = null;
            if (currentIndex == realList.size()){
                realList.add(new CountedFlighweight<T>());
            }
            // here the StackOverflowError catching is a pure formality, by next() we would never cause StackOverflow
            try {
                result = realList.get(currentIndex).get(calculate, currentIndex);
            } catch (StackOverflowError e) {
            }
            currentIndex++;
            return result;
        }

    }
    /**
     * if known is false, the value of reference is irrelevant
     * if known is true, and reference is not null, reference contains the data
     * if known is true, and reference is null, that means, that the appropriate data are corrupted in any way
     * calculation on corrupted data should result in corrupted data.
     * @author Pet
     *
     * @param <U>
     */
    public class CountedFlighweight<U>{
        private boolean known = false;
        private U reference;
        /**
         * used for initial values setting 
         */
        private void set(U value){
            reference = value;
            known = true;
        }
        /**
         * used for data retrieval or function counting and data saving if necessary
         * @param calculate
         * @param index
         * @return
         * @throws Exception
         */
        public U get(StackOverflowingFunction<Integer, U> calculate, int index) throws StackOverflowError{
            if (! known){
                if(calculate == null) {
                    reference = null;
                } else {
                    try {
                        reference = calculate.apply(index);
                    } catch (Exception e) {
                        reference = null;
                    }
                }
            }
            known = true;
            return reference;
        }
    }

    @FunctionalInterface
    public interface StackOverflowingFunction <K, U> {
        public U apply(K index) throws StackOverflowError;

    }
}

由于递归函数很容易遇到 StackOverflowError,我们应该组织递归,这样在这种情况下,整个递归序列将在没有真正遇到任何更改的情况下回滚并抛出异常。

FunctionalSequence 的使用可能是这样的:

    // by iterator:
    int index=0;
    Iterator<BigInteger> iterator = fiboSequence.iterator(); 
    while(index++<10){
        System.out.println(iterator.next());
    }

左右:

static private void tryFibo(FunctionalSequence<BigInteger> fiboSequence, int i){
    long startTime = System.nanoTime();
    long endTime;
    try {
        fiboSequence.get(i);
        endTime = System.nanoTime();
        System.out.println("repeated timing for f("+i+")=" + (endTime-startTime)/1000000.+" ns");
    } catch (StackOverflowError e) {
        endTime = System.nanoTime();
        //e.printStackTrace();
        System.out.println("failed counting f("+i+"), time=" + (endTime-startTime)/1000000.+" ns");
    }       
}

最后一个函数可以按如下方式使用:

    tryFibo(fiboSequence, 1100);
    tryFibo(fiboSequence, 100);
    tryFibo(fiboSequence, 100);
    tryFibo(fiboSequence, 200);
    tryFibo(fiboSequence, 1100);
    tryFibo(fiboSequence, 2100);
    tryFibo(fiboSequence, 2100);
    tryFibo(fiboSequence, 1100);
    tryFibo(fiboSequence, 100);
    tryFibo(fiboSequence, 100);
    tryFibo(fiboSequence, 200);
    tryFibo(fiboSequence, 1100);

以下是结果(堆栈限制为 256K 以供测试):

1
1
2
3
5
8
13
21
34
55
failed counting f(1100), time=3.555689 ns
repeated timing for f(100)=0.213156 ns
repeated timing for f(100)=0.002444 ns
repeated timing for f(200)=0.266933 ns
repeated timing for f(1100)=5.457956 ns
repeated timing for f(2100)=3.016445 ns
repeated timing for f(2100)=0.001467 ns
repeated timing for f(1100)=0.005378 ns
repeated timing for f(100)=0.002934 ns
repeated timing for f(100)=0.002445 ns
repeated timing for f(200)=0.002445 ns
repeated timing for f(1100)=0.003911 ns

看,f(i) 对同一索引的可重复调用几乎不需要任何时间 - 没有进行迭代。由于 StackOverflowError,我们无法立即达到 f(1100)。但是在我们达到一次 f(200) 之后,f(1100) 变得可达。我们成功了!

【讨论】:

    【解决方案2】:

    您的要求似乎是这样的:

    public class Fibonacci extends AbstractList<BigInteger> {
        @Override
        public Stream<BigInteger> stream() {
            return Stream.iterate(new BigInteger[]{ BigInteger.ONE, BigInteger.ONE },
               p->new BigInteger[]{ p[1], p[0].add(p[1]) }).map(p -> p[0]);
        }
        @Override
        public Iterator<BigInteger> iterator() {
            return stream().iterator();
        }
        @Override
        public int size() {
            return Integer.MAX_VALUE;
        }
        @Override
        public BigInteger get(int index) {
            return stream().skip(index).findFirst().get();
        }
    }
    

    它可以通过List 接口访问(它没有实现RandomAccess 是有充分理由的),因此,您可以通过get(n) 请求第n 个值。请注意,get 的实现暗示了如何在Integer.MAX_VALUE 之后的位置获取值。只需使用stream().skip(position).findFirst().get()

    小心!正如您所要求的,此列表是无限。不要要求它对所有元素都起作用的东西,例如甚至没有toString()。但是像下面这样的事情会顺利进行:

    System.out.println(new Fibonacci().subList(100, 120));
    

    for(BigInteger value: new Fibonacci()) {
        System.out.println(value);
        if(someCondition()) break;
    }   
    

    但是,当您必须处理大量元素序列并希望高效地完成时,您应该确保在迭代器或流上工作,以避免 O(n²) 重复调用 get 的复杂性。

    请注意,我将元素类型更改为BigInteger,因为当涉及到斐波那契数列和intlong 值类型时,考虑无限流是没有意义的。即使使用 long 值类型,序列在只有 92 个值之后就结束了,然后会发生溢出。


    更新:既然您明确表示您正在寻找一个惰性存储,您可以将上面的类更改如下:

    public class Fibonacci extends AbstractList<BigInteger> {
        final Map<BigInteger,BigInteger> values=new HashMap<>();
    
        public Fibonacci() {
            values.put(BigInteger.ONE, BigInteger.ONE);
            values.put(BigInteger.ZERO, BigInteger.ONE);
        }
    
        @Override
        public BigInteger get(int index) {
            return get(BigInteger.valueOf(index));
        }
        public BigInteger get(BigInteger index) {
            return values.computeIfAbsent(index, ix ->
                get(ix=ix.subtract(BigInteger.ONE)).add(get(ix.subtract(BigInteger.ONE))));
        }
    
        @Override
        public Stream<BigInteger> stream() {
            return Stream.iterate(BigInteger.ZERO, i->i.add(BigInteger.ONE)).map(this::get);
        }
        @Override
        public Iterator<BigInteger> iterator() {
            return stream().iterator();
        }
        @Override
        public int size() {
            return Integer.MAX_VALUE;
        }
    }
    

    我在这里使用BigInteger 作为键/索引来满足(理论上)无限的要求,尽管我们也可以将long 键用于所有实际用途。关键是最初的空存储:(现在使用long 进行示范):

    final Map<Long,BigInteger> values=new HashMap<>();
    

    使用应该结束每个递归的值进行预初始化(除非由于已经计算的值而提前结束):

    values.put(1L, BigInteger.ONE);
    values.put(0L, BigInteger.ONE);
    

    然后,我们可以通过以下方式请求一个惰性计算值:

    public BigInteger get(long index) {
        return values.computeIfAbsent(index, ix -> get(ix-1).add(get(ix-2)));
    }
    

    或委托给上述get 方法的流:

    LongStream.range(0, Long.MAX_VALUE).mapToObj(this::get);
    

    这会创建一个“实际上是无限的”流,而上面使用BigInteger 的完整示例类在理论上是无限的……

    Map 将记住序列的每个计算值。

    【讨论】:

    • 非常抱歉,这是一段非常有趣的代码,但不是我要说的。如果我们真的有一个惰性集合,那么递归只会计算它的任何项目一次。因此,如果我们第一次询问 F(n),复杂度最多为 O(n)。
    • 因为流被消费了,这段代码的复杂度为 O(n) 每次我们要求 F(n)。这是非常理想的形式。
    • @Gangnus:流不是存储。即使你可以多次使用一个流,它也不会变成一个存储,每个查询仍然有O(n)。如果您想存储计算值,则应将其包含在您的问题中。
    • 是的。而且我没有要求流。我已经解释了为什么流不是解决方案。
    • 但无论如何,你有一个不错的流,肯定有用。但不是为了这个问题。对不起
    【解决方案3】:

    我想不出一个好的通用解决方案,但如果您想专门访问前两个元素,这可以通过定义自定义 Spliterator 的非常简单的方式完成,如下所示:

    public static IntStream iterate(int first, int second, IntBinaryOperator generator) {
        Spliterator.OfInt spliterator = new AbstractIntSpliterator(Long.MAX_VALUE, 
                                                 Spliterator.ORDERED) {
            int prev1 = first, prev2 = second;
            int pos = 0;
    
            @Override
            public boolean tryAdvance(IntConsumer action) {
                if(pos < 2) {
                    action.accept(++pos == 1 ? prev1 : prev2);
                } else {
                    int next = generator.applyAsInt(prev1, prev2);
                    prev1 = prev2;
                    prev2 = next;
                    action.accept(next);
                }
                return true;
            }
        };
        return StreamSupport.intStream(spliterator, false);
    }
    

    用法:

    iterate(1, 1, Integer::sum).limit(20).forEach(System.out::println);
    

    【讨论】:

    • 1.它被使用消耗。 2. 使用只比闭包迭代的整个代码短一点。
    • 如果我需要 fibo(i),我该如何使用它?
    • @Gangnus, iterate(1, 1, Integer::sum).skip(i-1).findFirst().get().
    猜你喜欢
    • 2013-02-27
    • 1970-01-01
    • 2010-10-20
    • 2012-07-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-16
    • 2015-05-24
    相关资源
    最近更新 更多