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

问题描述 投票:14回答:4

实际上,我很惊讶我无法在这里找到答案,尽管也许我只是使用了错误的搜索词或其他内容。我能找到的最接近的是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方法。它确实需要更多的算术运算,因此在对性能非常敏感的情况下(例如我们的情况),当不需要舍入时,可以选择使用更简单的方法。我怀疑在大多数通用情况下,舍入开销可以忽略不计。

[请注意,为简化起见并希望专注于当前问题,我故意排除了用于处理“ InfinityNaNstart,[C0

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]

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

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

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

java double sequence
4个回答
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]
}

具有此签名(3个参数)的方法iterate已在Java 9中添加。因此,对于Java 8,代码如下所示

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

3
投票

我个人,我会把DoubleSequenceGenerator类缩短一些其他的东西,并且只使用一个sequence generator

方法,该方法包含使用想要的任何所需精度或不使用精度的选项完全:

在下面的生成器方法中,如果没有为可选的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]

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,还有一些提升性能的空间,但是这种方法本身是正确的。
  3. 您的错误:舍入错误(只需将while (mult*fraction < 1.0)更改为while (mult*fraction < 10.0),这应该可以解决)

      所有其他人都没有达到end ...好吧,也许他们只是不够细心以至无法阅读您代码中的注释
    1. 其他所有都慢一些。
    2. 只要将主循环中的条件从int < Double更改为int < int,就会显着提高代码速度
  4. 有人对此有更好的方法吗?
    嗯...以什么方式?
  5. 简单吗? @Evgeniy Khyst的generateSequenceDoubleStream看起来很简单。并且应该使用...但可能不会,因为接下来的两点

      精确? generateSequenceDoubleStream不是!但是仍然可以用模式start + step*i保存。而且start + step*i模式很精确。只有BigDouble和定点算术可以击败它。但是BigDouble很慢,并且手动定点运算很繁琐,可能不适合您的数据。顺便说一句,在精度方面,您可以这样娱乐自己:https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
    1. 速度...好吧,我们现在处于不稳定状态。查看此副本https://repl.it/repls/RespectfulSufficientWorker我目前还没有一个不错的测试平台,所以我使用了repl.it ...这完全不足以进行性能测试,但这不是重点。关键是-没有确切的答案。除了您可能无法完全解决的情况以外,您绝对不应该使用BigDecimal(继续阅读)。
    2. 我已经尝试过并为大量投入进行优化。和您的原始代码,但有一些小的更改-最快。但是也许您需要大量的小List?那可能是一个完全不同的故事。

      此代码对我来说很简单,并且足够快:

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

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

    尝试从Double切换到double,如果确实需要它们,则可以根据测试判断再次切换回去,它可能会更快。 (但是,请不要相信我,请在您的环境中使用您的数据自己尝试一下。正如我说的那样-repl.it很容易成为基准测试)]

    1. 一个小魔术:Math.round()的单独循环...也许与数据局部性有关。我不建议这样做-结果非常不稳定。但这很有趣。
    2. 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;

    您绝对应该认为自己比较懒惰,可以按需生成数字,而无需存储在List
  • [我怀疑在大多数一般使用情况下,舍入开销可以忽略不计。
    [如果您怀疑某事-测试它:-)我的回答是“是”,但是再次……不要相信我。测试一下。
  • 所以,回到主要问题:还有更好的方法吗?当然是!但这取决于。

    选择

      Big十进制,如果您需要非常
    1. big个数字and非常small个数字。但是,如果将它们转换回Double,甚至更多,请使用“接近”数量级的数字-不需要它们!签出相同的副本:https://repl.it/repls/RespectfulSufficientWorker-上一次测试表明会出现[[结果无差异,但是会降低速度。
    2. 根据数据属性,任务和环境进行一些微优化。如果能从5-10%的性能提升中获益不多,则最好使用简短的代码。不要浪费时间如果可能并且值得的话,也许使用定点算法。
  • 除此之外,您还可以。
  • PS。副本中还有一个Kahan Summation Formula实现...只是为了好玩。 https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346并且有效-您

    can减少求和错误

  • © www.soinside.com 2019 - 2024. All rights reserved.