開始、終了、およびステップが指定された値のList <Double>シーケンスを生成する最良の方法?


14

ここでこれに対する答えを見つけることができなかったのは、実際には非常に驚いていますが、おそらく間違った検索用語などを使用しているだけです。私が見つけることができる最も近いのはこれですが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> 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]

この種の機能を提供する既存のライブラリはすでにありますか?

そうでない場合、私のアプローチに問題はありますか?

誰かこれにもっと良いアプローチがありますか?

回答:


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つのパラメーター)で反復するメソッドはJava 9で追加されました。したがって、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 lengthIntStream.iterateおよびの同様のエラーStream.iterate。また、non-static method doubleValue() cannot be referenced from a static context
NanoWizard 2019

1
回答にJava 11コードが含まれている
Evgeniy Khyst

@NanoWizardはJava 8のサンプルで回答を拡張しました
Evgeniy Khyst

3引数は、反復子は、Java 9で追加されました
するThorbjörnRavnアンデルセン

3

私個人的には、DoubleSequenceGeneratorクラスを他の利点のために少し短くし、必要な精度を使用するか、精度をまったく使用しないオプションを含むシーケンスジェネレーターメソッドを1つだけ使用します。

以下のジェネレーターメソッドでは、オプションのsetPrecisionパラメーターに何も(または0 未満の値)が指定されていない場合、小数精度の丸めは実行されません。精度値として0が指定されている場合、数値は最も近い整数に丸められます(つまり、89.674は90.0に丸められます)。0より大きい特定の精度値が指定された場合、値はその10進精度に変換されます。

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 を使用しvalてもBigDecimal、丸め処理を除いて、使用しても正確なメリットは得られません。
NanoWizard 2019

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のスケールを返します。ゼロまたは正の場合、位取りは小数点の右側の桁数です。負の場合、数値のスケーリングされていない値に、スケールの否定の累乗に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]

2
  1. この種の機能を提供する既存のライブラリはすでにありますか?

    申し訳ありませんが、わかりませんが、他の回答とそれらの比較的単純さから判断すると、いいえ、ありません。必要なし。よくほとんど...

  2. そうでない場合、私のアプローチに問題はありますか?

    はいといいえ。少なくとも1つのバグがあり、パフォーマンスが向上する余地がありますが、アプローチ自体は正しいです。

    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つのポイントのために、おそらく使用しないでください
    2. 正確?generateSequenceDoubleStreamではありません!しかし、それでもパターンで保存することができますstart + step*i。そして、start + step*iパターンは正確です。BigDouble固定小数点演算のみがこれに勝ることができます。ただし、BigDoublesは遅く、手動の固定小数点演算は面倒であり、データに適さない場合があります。ちなみに、精度に関しては、https//docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.htmlで楽しもうことができます。
    3. 速度...さて、今私たちは不安定な立場にいます。このreplをチェックしてください 。ポイントは-明確な答えはありません。おそらくあなたの質問から完全に明確ではないあなたの場合を除いて、あなたは間違いなく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. ちょっとした魔法:別のループfor 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. 非常に大きな非常に小さな数が必要な場合は、Big Decimalを選択します。しかし、それらをにキャストする場合、それ以上の場合は、「近い」等級の数で使用してください-それらの必要はありません!同じreplをチェックアウトします。https://repl.it/repls/RespectfulSufficientWorker- 最後のテストでは、結果違いはありませんが、速度が大幅に低下しています。Double
  2. データプロパティ、タスク、および環境に基づいて、いくつかのマイクロ最適化を行います。
  3. 5〜10%のパフォーマンス向上で得られるものがあまりない場合は、短くて単純なコードを使用してください。時間を無駄にしないでください
  4. 可能であれば、その価値がある場合は、固定小数点演算を使用することもできます。

それ以外は大丈夫です。

PS。replにはKahan Summation Formulaの実装もあります。https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346動作します- 合計エラー軽減できます

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.