5

我正在尝试学习使用 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 /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;
}

但我碰巧知道旧方法使用了近 800000 个术语,并且对流进行限制违背了我的目的。问题是我没有看到除使用之外的其他方法来切断流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;
}

跟踪表明当看到最后一个术语时确实发生了一些事情,但没有什么好处:在一种情况下,计算停止但程序仍然挂起,在另一种情况下,它给了我一个可爱的小故障转储,我可以报告给甲骨文。

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

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

4

3 回答 3

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();
于 2014-04-03T11:22:01.850 回答
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++); }
}

看起来这不容易“并行化”,我倾向于得出结论,您的初始循环看起来并不那么糟糕......

于 2014-04-02T23:11:12.847 回答
0

相关问题中提到,现在有了Java 9,我们有了 Stream。takeWhile(Predicate)很酷,虽然为了单一特性而迁移到 JDK 9可能不合理,所以我提出了另一种方法。


正如你所写:

但我碰巧知道旧方法使用了近 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;
}

或者,可以避免对这个序列进行硬编码,并实现在旅途中找到它的下一个术语。对于斐波那契数列,请参见此处的示例。

此外,它可能是另一个序列,例如指数: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();
}

附言:

虽然也许有更有效的序列来找到它,但我倾向于相信这个序列接近最优,因为斐波那契序列比二次序列更具动态性,并且比指数 (2^x, 1.6^x; 但不是 1.5^ x) - 看看我的谦虚比较


更实用的方法:

(但它也没有被提及)

 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);

希望能帮助到你 ;]

于 2018-06-24T09:19:47.060 回答