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


14

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

我算起来也是在图书馆这样的一些方法已经某处,但如果让我无法轻松地找到它(再次,也许我只是用错了搜索词或某事)。因此,这是我在过去几分钟内自行完成的操作:

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为步长分数可能导致明显的浮点错误的情况添加了该方法。它确实需要更多的算术运算,因此在对性能非常敏感的情况下(例如我们的情况),在不需要舍入时可以选择使用更简单的方法,这是很好的选择。我怀疑在大多数一般用例中,舍入开销可以忽略不计。

注意,我有意排除的逻辑,用于处理“异常”的参数,如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]

是否已经存在提供这种功能的现有库?

如果没有,我的方法是否有任何问题?

有谁有更好的方法吗?

Answers:


17

使用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个参数)。因此,对于Java 8,代码看起来像

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

这是更好的方法。
Vishwa Ratna

我看到几个编译错误(JDK 1.8.0): error: method iterate in interface DoubleStream cannot be applied to given types; return DoubleStream.iterate(start, d -> d <= end, d -> d + step) required: double,DoubleUnaryOperator. found: double,(d)->d <= end,(d)->d + step. reason: actual and formal argument lists differ in length。类似的错误的IntStream.iterateStream.iterate。另外,non-static method doubleValue() cannot be referenced from a static context
NanoWizard,

1
答案包含Java 11代码
Evgeniy Khyst

@NanoWizard使用Java 8示例扩展了答案
Evgeniy Khyst,

在Java中9添加了三个参数的迭代器
托尔比约恩Ravn的安德森

3

我个人来说,我会把DoubleSequenceGenerator类缩短一些其他优点,并只使用一种序列生成器方法,该方法包含使用所需的任何所需精度或完全不使用精度的选项:

在下面的生成器方法中,如果没有为可选的setPrecision参数提供任何值(或任何小于 0的值),则不进行十进制精度舍入。如果为精度值提供了0,则数字将四舍五入到最接近的整数(即: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]

有趣的想法,尽管我看到了一些问题。1.通过val在每次迭代中加和,您将获得累加的精度损失。对于非常大的序列,最后几个数字的误差可能很大。2.重复通话BigDecimal.valueOf()相对昂贵。通过将输入转换为BigDecimals并使用BigDecimalfor ,可以获得更好的性能(和精度)val。实际上,通过使用doublefor valBigDecimal除了舍入之外,实际上并没有获得任何精度上的好处。
NanoWizard

2

尝试这个。

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

返回此BigDecimal的小数位数。如果为零或正数,则小数位数是小数点右边的位数。如果为负,则该数字的未标度值乘以十,即标度取反的幂。例如,小数位数为-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]

2
  1. 是否已经存在提供这种功能的现有库?

    抱歉,我不知道,但是根据其他答案以及它们的相对简单性来判断-不,没有。没必要。好吧,差不多...

  2. 如果没有,我的方法是否有任何问题?

    是的,没有。您至少有一个bug,还有一些提升性能的空间,但是这种方法本身是正确的。

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

    嗯...用什么方式?

    1. 简单?generateSequenceDoubleStream@Evgeniy Khyst的内容看起来很简单。应该使用...但可能不会,因为接下来的两点
    2. 精确?generateSequenceDoubleStream不是!但是还是可以用模式保存的start + step*i。而start + step*i模式是精确的。只有BigDouble定点算术可以击败它。但是BigDoubles很慢,并且手动定点算法很繁琐,可能不适用于您的数据。顺便说一句,在精度方面,您可以通过以下方式来娱乐自己:https : //docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
    3. 速度...好吧,现在我们处于不稳定状态。看看这个repl https://repl.it/repls/RespectfulSufficientWorker 我现在还没有一个不错的测试平台,所以我使用了repl.it ...这完全不足以进行性能测试,但这不是重点。关键是-没有确切的答案。除了您可能无法完全弄清您的情况外,您绝对不应该使用BigDecimal(继续阅读)。

      我已经尝试过并为大量输入进行优化。和您的原始代码,进行一些细微的更改-最快。但是,也许您需要大量的small 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. 尝试从切换Doubledouble,如果确实需要它们,可以根据测试判断再次切换回去,它可能仍然会更快。(但是,请不要相信我,请在您的环境中使用您自己的数据尝试一下。正如我所说的那样-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. 我怀疑在大多数一般用例中,舍入开销可以忽略不计。

    如果您怀疑某事,请进行测试:-)我的回答是“是”,但再次……不相信我。测试一下。

因此,回到主要问题:还有更好的方法吗?
当然是!
但这要看情况。

  1. 如果需要非常大的数字非常小的数字,请选择十进制。但是,如果将其转换为,甚至更多,请使用数量“接近”的数字-不需要它们!签出相同的repl:https ://repl.it/repls/RespectfulSufficientWorker- 上一次测试显示结果没有差异,但是速度有所损失。Double
  2. 根据您的数据属性,任务和环境进行一些微优化。
  3. 如果从5-10%的性能提升中获益不多,则最好使用简短的代码。不要浪费时间
  4. 如果可以的话,也许值得使用定点算法。

除此之外,你还好。

PS。复制代码中还有一个Kahan Summation Formula实现...只是为了好玩。https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346并且有效-您可以减轻求和错误

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.