在给定开始、结束和步骤的情况下生成 List<Double> 值序列的最佳方法?

Best way to generate a List<Double> sequence of values given start, end, and step?

我真的很惊讶我无法在这里找到这个问题的答案,虽然也许我只是使用了错误的搜索词或其他东西。我能找到的最接近的是 this,但他们询问如何生成具有特定步长的特定范围的 double,答案也是如此。我需要一些可以生成具有任意开始、结束和步长大小的数字的东西。

我想 已经在某处的库中有这样的方法了,但如果是这样我就没法轻易找到它了(再一次,也许我是只是使用了错误的搜索词或其他东西)。所以这就是我在过去几分钟里自己做的:

import java.lang.Math;
import java.util.List;
import java.util.ArrayList;

public class DoubleSequenceGenerator {


     /**
     * Generates a List of Double values beginning with `start` and ending with
     * the last step from `start` which includes the provided `end` value.
     **/
    public static List<Double> generateSequence(double start, double end, double step) {
        Double numValues = (end-start)/step + 1.0;
        List<Double> sequence = new ArrayList<Double>(numValues.intValue());

        sequence.add(start);
        for (int i=1; i < numValues; i++) {
          sequence.add(start + step*i);
        }

        return sequence;
    }

    /**
     * Generates a List of Double values beginning with `start` and ending with
     * the last step from `start` which includes the provided `end` value.
     * 
     * Each number in the sequence is rounded to the precision of the `step`
     * value. For instance, if step=0.025, values will round to the nearest
     * thousandth value (0.001).
     **/
    public static List<Double> generateSequenceRounded(double start, double end, double step) {

        if (step != Math.floor(step)) {
            Double numValues = (end-start)/step + 1.0;
            List<Double> sequence = new ArrayList<Double>(numValues.intValue());

            double fraction = step - Math.floor(step);
            double mult = 10;
            while (mult*fraction < 1.0) {
                mult *= 10;
            }

            sequence.add(start);
            for (int i=1; i < numValues; i++) {
              sequence.add(Math.round(mult*(start + step*i))/mult);
            }

            return sequence;
        }

        return generateSequence(start, end, step);
    }

}

这些方法 运行 一个简单的循环,将 step 乘以序列索引并添加到​​ start 偏移量。这减轻了连续递增时会发生的复合浮点错误(例如在每次迭代时将 step 添加到变量)。

我添加了 generateSequenceRounded 方法来处理小数步长可能导致明显浮点错误的情况。它确实需要更多的算法,所以在像我们这样对性能极其敏感的情况下,在不需要舍入时可以选择使用更简单的方法是很好的。我怀疑在大多数一般用例中,舍入开销可以忽略不计。

请注意,我有意排除了处理 "abnormal" 参数的逻辑,例如 InfinityNaNstart > end 或否定 step 大小为了简单和希望专注于手头的问题。

下面是一些示例用法和相应的输出:

System.out.println(DoubleSequenceGenerator.generateSequence(0.0, 2.0, 0.2))
System.out.println(DoubleSequenceGenerator.generateSequenceRounded(0.0, 2.0, 0.2));
System.out.println(DoubleSequenceGenerator.generateSequence(0.0, 102.0, 10.2));
System.out.println(DoubleSequenceGenerator.generateSequenceRounded(0.0, 102.0, 10.2));
[0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6, 1.8, 2.0]
[0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0]
[0.0, 10.2, 20.4, 30.599999999999998, 40.8, 51.0, 61.199999999999996, 71.39999999999999, 81.6, 91.8, 102.0]
[0.0, 10.2, 20.4, 30.6, 40.8, 51.0, 61.2, 71.4, 81.6, 91.8, 102.0]

是否已有提供此类功能的库?

如果不是,我的方法有什么问题吗?

有人对此有更好的方法吗?

就我个人而言,我会缩短 DoubleSequenceGenerator class 一些其他好东西,并且只使用一个 序列生成器 方法,包含使用任何所需精度或根本不使用精度的选项:

在下面的生成器方法中,如果没有向可选的 setPrecision 参数提供任何值(或任何 小于 0 的值),则不会进行小数精度四舍五入。如果为精度值提供了 0,则数字将四舍五入到最接近的 whole 数字(即:89.674 四舍五入到 90.0)。如果提供的特定精度值 大于 0,则值将转换为该十进制精度。

这里使用 BigDecimal 是为了......嗯......精度:

import java.util.List;
import java.util.ArrayList;
import java.math.BigDecimal;
import java.math.RoundingMode;

public class DoubleSequenceGenerator {

     public static List<Double> generateSequence(double start, double end, 
                                          double step, int... setPrecision) {
        int precision = -1;
        if (setPrecision.length > 0) {
            precision = setPrecision[0];
        }
        List<Double> sequence = new ArrayList<>();
        for (double val = start; val < end; val+= step) {
            if (precision > -1) {
                sequence.add(BigDecimal.valueOf(val).setScale(precision, RoundingMode.HALF_UP).doubleValue());
            }
            else {
                sequence.add(BigDecimal.valueOf(val).doubleValue());
            }
        }
        if (sequence.get(sequence.size() - 1) < end) { 
            sequence.add(end); 
        }
        return sequence;
    }    

    // Other class goodies here ....
}

在 main() 中:

System.out.println(generateSequence(0.0, 2.0, 0.2));
System.out.println(generateSequence(0.0, 2.0, 0.2, 0));
System.out.println(generateSequence(0.0, 2.0, 0.2, 1));
System.out.println();
System.out.println(generateSequence(0.0, 102.0, 10.2, 0));
System.out.println(generateSequence(0.0, 102.0, 10.2, 0));
System.out.println(generateSequence(0.0, 102.0, 10.2, 1));

并且控制台显示:

[0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2, 1.4, 1.5999999999999999, 1.7999999999999998, 1.9999999999999998, 2.0]
[0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0]
[0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0]

[0.0, 10.2, 20.4, 30.599999999999998, 40.8, 51.0, 61.2, 71.4, 81.60000000000001, 91.80000000000001, 102.0]
[0.0, 10.0, 20.0, 31.0, 41.0, 51.0, 61.0, 71.0, 82.0, 92.0, 102.0]
[0.0, 10.2, 20.4, 30.6, 40.8, 51.0, 61.2, 71.4, 81.6, 91.8, 102.0]

可以使用 Java 11 Stream API.

轻松生成序列

直接的方法是使用 DoubleStream:

public static List<Double> generateSequenceDoubleStream(double start, double end, double step) {
  return DoubleStream.iterate(start, d -> d <= end, d -> d + step)
      .boxed()
      .collect(toList());
}

在具有大量迭代的范围内,double 精度误差可能会累积,导致接近范围末尾的误差更大。 可以通过切换到 IntStream 并使用整数和单双乘数来最小化错误:

public static List<Double> generateSequenceIntStream(int start, int end, int step, double multiplier) {
  return IntStream.iterate(start, i -> i <= end, i -> i + step)
      .mapToDouble(i -> i * multiplier)
      .boxed()
      .collect(toList());
}

要完全消除 double 精度错误,可以使用 BigDecimal

public static List<Double> generateSequenceBigDecimal(BigDecimal start, BigDecimal end, BigDecimal step) {
  return Stream.iterate(start, d -> d.compareTo(end) <= 0, d -> d.add(step))
      .mapToDouble(BigDecimal::doubleValue)
      .boxed()
      .collect(toList());
}

示例:

public static void main(String[] args) {
  System.out.println(generateSequenceDoubleStream(0.0, 2.0, 0.2));
  //[0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2, 1.4, 1.5999999999999999, 1.7999999999999998, 1.9999999999999998]

  System.out.println(generateSequenceIntStream(0, 20, 2, 0.1));
  //[0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6, 1.8, 2.0]

  System.out.println(generateSequenceBigDecimal(new BigDecimal("0"), new BigDecimal("2"), new BigDecimal("0.2")));
  //[0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0]
}

在 Java 9 中添加了具有此签名(3 个参数)的方法 iterate。因此,对于 Java 8,代码看起来像

DoubleStream.iterate(start, d -> d + step)
    .limit((int) (1 + (end - start) / step))

试试这个。

public static List<Double> generateSequenceRounded(double start, double end, double step) {
    long mult = (long) Math.pow(10, BigDecimal.valueOf(step).scale());
    return DoubleStream.iterate(start, d -> (double) Math.round(mult * (d + step)) / mult)
                .limit((long) (1 + (end - start) / step)).boxed().collect(Collectors.toList());
}

这里,

int java.math.BigDecimal.scale()

Returns 这个 BigDecimal 的小数位数。如果为零或正数,则小数位数是小数点右边的位数。如果为负数,则将数字的未缩放值乘以 10 的缩放负数次方。例如,-3 的比例表示未缩放的值乘以 1000。

在main()中

System.out.println(generateSequenceRounded(0.0, 102.0, 10.2));
System.out.println(generateSequenceRounded(0.0, 102.0, 10.24367));

和输出:

[0.0, 10.2, 20.4, 30.6, 40.8, 51.0, 61.2, 71.4, 81.6, 91.8, 102.0]
[0.0, 10.24367, 20.48734, 30.73101, 40.97468, 51.21835, 61.46202, 71.70569, 81.94936, 92.19303]
  1. Is there an existing library that provides this kind of functionality already?

    抱歉,我不知道,但从其他答案及其相对简单性来看 - 不,没有。不需要。嗯,差不多...

  2. If not, are there any issues with my approach?

    是也不是。您至少有一个错误,并且有一定的性能提升空间,但方法本身是正确的。

    1. 您的错误:舍入错误(只需将 while (mult*fraction < 1.0) 更改为 while (mult*fraction < 10.0) 即可解决)
    2. 所有其他人都没有达到 end...好吧,也许他们只是不够细心,无法阅读您代码中的注释
    3. 其他的都比较慢。
    4. 只需将主循环中的条件从 int < Double 更改为 int < int 即可显着提高代码速度
  3. Does anyone have a better approach to this?

    嗯...以什么方式?

    1. 简单吗? @Evgeniy Khyst 的 generateSequenceDoubleStream 看起来很简单。并且应该使用...但也许不会,因为接下来的两点
    2. 准确吗? generateSequenceDoubleStream 不是!但仍然可以用模式 start + step*i 保存。 start + step*i 模式是精确的。只有BigDouble和定点运算能打败它。但是 BigDoubles 很慢,手动定点运算很乏味,可能不适合您的数据。 顺便说一句,关于精度的问题,你可以自娱自乐:https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
    3. 速度...好吧,现在我们站稳了脚跟。 查看此回复 https://repl.it/repls/RespectfulSufficientWorker 我现在没有像样的测试台,所以我使用了repl.it...,这对于性能测试来说是完全不够的,但这不是重点。关键是——没有确定的答案。除了可能在你的情况下,你的问题并不完全清楚,你绝对不应该使用 BigDecimal (进一步阅读)。

      我试过针对大投入进行游戏和优化。和你的原始代码,有一些小的变化 - 最快。但也许您需要大量的小 Lists?那么这可能是一个完全不同的故事。

      这段代码对我来说很简单,而且速度足够快:

        public static List<Double> genNoRoundDirectToDouble(double start, double end, double step) {
        int len = (int)Math.ceil((end-start)/step) + 1;
        var sequence = new ArrayList<Double>(len);
        sequence.add(start);
        for (int i=1 ; i < len ; ++i) sequence.add(start + step*i);
        return sequence;
        }
    

    如果您更喜欢更优雅的方式(或者我们应该称之为惯用方式),我个人建议:

    public static List<Double> gen_DoubleStream_presice(double start, double end, double step) {
        return IntStream.range(0, (int)Math.ceil((end-start)/step) + 1)
            .mapToDouble(i -> start + i * step)
            .boxed()
            .collect(Collectors.toList());
    }
    

    无论如何,可能的性能提升是:

    1. 试试从Double切换到double,如果真的需要,可以再切换回去,从测试来看,可能还是会更快。 (但不要相信我,请在您的环境中使用您的数据自己尝试。正如我所说 - repl.it 基准测试很糟糕)
    2. 一点魔法:Math.round() 的单独循环...也许它与数据局部性有关。我不推荐这个——结果非常不稳定。不过很好玩

      double[] sequence = new double[len];
      for (int i=1; i < len; ++i) sequence[i] = start + step*i;
      List<Double> list = new ArrayList<Double>(len);
      list.add(start);
      for (int i=1; i < len; ++i) list.add(Math.round(sequence[i])/mult);
      return list;
      
    3. 你绝对应该考虑更懒惰,按需生成数字而不存储然后在 Lists

  4. I suspect that in most general use cases the rounding overhead would be negligible.

    如果您怀疑某些东西 - 测试它:-) 我的答案是 "Yes",但是再次......不要相信我。测试一下。

所以,回到主要问题:有没有更好的方法?
是的,当然!
但这取决于。

  1. 如果您需要非常的数字非常[=66=,请选择小数]small 个数字。但是,如果您将它们转换回 Double,甚至更多,则将其与 "close" 数量级的数字一起使用 - 不需要它们!检查相同的 repl:https://repl.it/repls/RespectfulSufficientWorker - 上次测试表明 结果没有差异 ,但挖掘速度有所下降。
  2. 根据您的数据属性、任务和环境进行一些微优化。
  3. 如果从 5-10% 的性能提升中获益不大,则更喜欢简短的代码。不要浪费你的时间
  4. 如果可以并且值得的话,也许使用定点运算。

除此之外,你还好。

PS。在 repl 中还有一个 Kahan Summation Formula 实现......只是为了好玩。 https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346 它有效 - 你 可以 减少求和错误