使用流来计算无限总和

时间:2014-04-02 20:54:22

标签: java java-8 java-stream

我正在尝试学习使用Stream API的细节,我给自己的一个任务是尝试编写一个带有无限DoubleStream的方法并尝试计算总和(假设它收敛)。也就是说,我想写一个方法

public static double infiniteSum(DoubleStream ds) { ... }

我可以打电话给

double sum = infiniteSum(IntStream.iterate(1, (i -> i + 1))
                                  .mapToDouble(n -> 1 / ((double)n * n)));

得到总和(1 + 1/2 2 + 1/3 2 + ...)=ζ(2)=π 2 < / SUP> / 6

我粗暴的方法:旧方法:

public static void yeOldeWaye() {
    double sum = 0;
    for (double n = 1; ; n++) {
        double term = 1 / (n * n);
        if (Math.abs(term) <= 1e-12 * Math.abs(sum)) {
            break;
        }
        sum += term;
    }
    System.out.println(sum);
}

给我一​​个精确到5个地方的结果。

我可以使用iterator()

以黑客方式实施该方法
public static double infiniteSum1(DoubleStream ds) {
    double sum = 0;
    PrimitiveIterator.OfDouble it = ds.iterator();
    while (true) {
        double term = it.next();
        if (Math.abs(term) <= 1e-12 * Math.abs(sum)) {
            break;
        }
        sum += term;
    }
    return sum;
}

但这只是感觉就像回归旧的方式,我正在寻找一种方法,使用更多的方式使用流,或者其他东西。

这会产生正确的结果:

private static class DoubleAccumulator {
    public double sum;
    public DoubleAccumulator() {
        sum = 0;
    }
}

public static double infiniteSum(DoubleStream ds) {
    DoubleAccumulator summer = ds.limit(800000).collect
        (DoubleAccumulator::new,
         (s, d) -> s.sum += d,
         (s1, s2) -> s1.sum += s2.sum);
    return summer.sum;
}

但我碰巧知道旧方法使用了将近80万个术语,并且对流的限制违背了我的目的。问题是除了使用limit()之外我没有办法切断流,这意味着我必须事先知道我将要拥有多少个术语;我没有看到根据我在流中看到的计算条件来停止流的方法。

这不起作用:

public static double infiniteSum(DoubleStream ds) {
    DoubleAccumulator summer = ds.collect
        (DoubleAccumulator::new,
         (s, d) -> { if (Math.abs(d) <= 1e-12 * Math.abs(s.sum)) {
                        ds.close();  // AAACK
                     } else
                        s.sum += d;
                   },
         (s1, s2) -> s1.sum += s2.sum);
    return summer.sum;
}

跟踪表明在看到最后一个术语时会发生某些事情,但没有什么好处:在一种情况下,计算停止但程序仍然挂起,在另一种情况下,它给了我一个可爱的小崩溃转储,我得到报告给Oracle。

那么有没有办法完成我正在寻找的那种东西?

(注意:我现在正在假设串行流。但我认为这是一种可以从并行性中受益的问题,一旦我弄清楚如何使其工作。)

3 个答案:

答案 0 :(得分:4)

如果终止条件与Collector的结果之间存在这种依赖关系,则使用可变状态是不可避免的。但只要您不需要并行执行,实现就可以直接执行:

class MutableDouble {
    double sum;
}
MutableDouble sumHolder=new MutableDouble();
DoubleStream ds=IntStream.iterate(1, (i -> i + 1))
                         .mapToDouble(n -> 1 / ((double)n * n));
ds.peek(term -> sumHolder.sum+=term)
  .filter(term -> Math.abs(term)<1e-12*Math.abs(sumHolder.sum))
  .findFirst();
System.out.println(sumHolder.sum);

因为在比较之前总结一下它与原始逻辑不完全相同,而是像:

double sum = 0;
for (double n = 1; ; n++) {
    double term = 1 / (n * n);
    sum += term;
    if (Math.abs(term) <= 1e-12 * Math.abs(sum)) {
        break;
    }
}

如果你坚持原来的逻辑,它必须稍微复杂一些:

class MutableDouble {
    double sum, next;
    void add(double d) {
        sum=next;
        next+=d;
    }
}
MutableDouble sumHolder=new MutableDouble();
DoubleStream ds=IntStream.iterate(1, (i -> i + 1))
                         .mapToDouble(n -> 1 / ((double)n * n));
ds.peek(sumHolder::add)
  .filter(term -> Math.abs(term)<1e-12*Math.abs(sumHolder.sum))
  .findFirst();

答案 1 :(得分:1)

我们假设你知道结果是~1。所以你的比较:term <= 1e-12 * sum大致相当于term <= 1e-12

现在你不需要每一步的总和来简化问题(否则你可以跟踪迭代器中的总和)。然后可以写出来:

public static void yeStreamsWaye() {
    System.out.println(stream().sum());
}

private static DoubleStream stream() {
    return StreamSupport.doubleStream(Spliterators.spliteratorUnknownSize(new Piterator(),
            Spliterator.ORDERED | Spliterator.IMMUTABLE | Spliterator.NONNULL), false);
}

static class Piterator implements PrimitiveIterator.OfDouble {
    private double t = 1;
    @Override public boolean hasNext() { return 1 / (t * t) > 1e-12; }
    @Override public double nextDouble() { return 1 / (t * t++); }
}

看起来这并不容易&#34;并行化&#34;而且我倾向于得出结论,你的初始循环看起来并不那么糟糕......

答案 2 :(得分:0)

related question中提到,现在使用 Java 9 ,我们有了Stream。takeWhile(Predicate)很棒,尽管可以将Jem 9的单个功能迁移到 是不合理的,所以我建议使用another方法。


如您所写:

  

但是我碰巧知道旧方法使用了将近800000个术语,并且对数据流进行限制无法达到我的目的。问题是,除了使用limit()之外,我没有其他方法可以切断流,这意味着我必须事先知道我将要拥有多少个术语。我看不到一种基于根据在流中看到的内容计算出的条件来停止流的方法。

可以用一种简单的方法从数学上解决以下问题:找到最相关的术语数量:

static int numOfRelevantTerms(IntToDoubleFunction termRule, double precision) {
    int[] fibonacci = {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};
    for (int i : fibonacci) {
        double term = termRule.applyAsDouble(i);
        if (term < precision) {
            return i;
        }
    }
    return -1;
}

或者,可以避免对该序列进行硬编码,并实现在旅途中查找下一个术语。有关斐波那契数列,请参见示例here

另外,它可能是另一个序列,例如指数:8、16、32、64,... 然后,找到下一个词是非常容易的(尽管步伐很大):

for (int n = 8; ; n *= 2) {
    if (termRule.applyAsDouble(n) < precision) {
        return n;
    }
}

最后:

static double findSum(IntToDoubleFunction termRule, double precision) {
    int lastInt = numOfRelevantTerms(termRule, precision);
    return IntStream.range(1, lastInt)
            .mapToDouble(termRule)
            .sum();
}

Ps:

尽管也许有更高效的序列可以找到它,但我倾向于认为这一序列接近于最优,因为斐波那契序列比二次序列动态性更强,比指数序列更平衡(2 ^ x,1.6 ^ x;但是不是1.5 ^ x)-看到我的humble comparison


更实用的方法:

(但也没有提到)

 DoubleAdder sum = new DoubleAdder();
 IntStream.closedRange(1, Integer.MAX_VALUE)
        .mapToDouble(i -> 1.0 / i)
        .peek(System.out::println)
        .peek(sum::add)
        .anyMatch(term -> term < 0.01);

希望有帮助;]