Cara terbaik untuk menghasilkan urutan <Double> Daftar nilai yang diberikan mulai, akhir, dan langkah?

14

Saya sebenarnya sangat terkejut saya tidak dapat menemukan jawaban untuk ini di sini, meskipun mungkin saya hanya menggunakan istilah pencarian yang salah atau sesuatu. Yang paling dekat yang bisa saya temukan adalah ini , tetapi mereka bertanya tentang menghasilkan kisaran doubles tertentu dengan ukuran langkah spesifik, dan jawaban memperlakukannya seperti itu. Saya membutuhkan sesuatu yang akan menghasilkan angka dengan ukuran awal, akhir dan langkah yang sewenang-wenang.

Saya pikir ada memiliki menjadi beberapa metode seperti ini di perpustakaan di suatu tempat sudah, tapi kalau jadi saya tidak dapat menemukannya dengan mudah (sekali lagi, mungkin aku hanya menggunakan istilah pencarian yang salah atau sesuatu). Jadi inilah yang saya lakukan sendiri dalam beberapa menit terakhir untuk melakukan ini:

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

}

Metode-metode ini menjalankan loop sederhana mengalikan stepdengan indeks urutan dan menambahkan ke startoffset. Ini mengurangi kesalahan floating-point majemuk yang akan terjadi dengan peningkatan berkelanjutan (seperti menambahkan stepvariabel ke setiap iterasi).

Saya menambahkan generateSequenceRoundedmetode untuk kasus-kasus di mana ukuran langkah fraksional dapat menyebabkan kesalahan floating-point. Memang diperlukan aritmatika sedikit lebih, sehingga dalam situasi yang sangat sensitif seperti kita, bagus untuk memiliki pilihan menggunakan metode yang lebih sederhana ketika pembulatan tidak diperlukan. Saya menduga bahwa dalam sebagian besar kasus penggunaan umum, overhead pembulatan akan diabaikan.

Perhatikan bahwa saya sengaja dikeluarkan logika untuk menangani "abnormal" argumen seperti Infinity, NaN, start> end, atau negatif stepukuran untuk kesederhanaan dan ingin fokus pada pertanyaan di tangan.

Berikut beberapa contoh penggunaan dan output yang sesuai:

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]

Apakah sudah ada perpustakaan yang menyediakan fungsionalitas semacam ini?

Jika tidak, apakah ada masalah dengan pendekatan saya?

Apakah ada yang punya pendekatan yang lebih baik untuk ini?

NanoWizard
sumber

Jawaban:

17

Urutan dapat dibuat dengan mudah menggunakan Java 11 Stream API.

Pendekatan langsung adalah menggunakan 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());
}

Pada rentang dengan sejumlah besar iterasi, doublekesalahan presisi dapat terakumulasi yang menghasilkan kesalahan lebih besar lebih dekat ke ujung rentang. Kesalahan dapat diminimalkan dengan beralih ke IntStreamdan menggunakan bilangan bulat dan pengganda ganda tunggal:

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

Untuk menghilangkan doublekesalahan presisi sama sekali, BigDecimalbisa digunakan:

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

Contoh:

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]
}

Metode iterate dengan tanda tangan ini (3 parameter) telah ditambahkan di Java 9. Jadi, untuk Java 8 kodenya seperti

DoubleStream.iterate(start, d -> d + step)
    .limit((int) (1 + (end - start) / step))
Evgeniy Khyst
sumber
Ini pendekatan yang lebih baik.
Vishwa Ratna
Saya melihat beberapa kesalahan kompilasi (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. Kesalahan serupa untuk IntStream.iteratedan Stream.iterate. Juga non-static method doubleValue() cannot be referenced from a static context,.
NanoWizard
1
Jawaban berisi kode Java 11
Evgeniy Khyst
@NanoWizard memperluas jawaban dengan sampel untuk Java 8
Evgeniy Khyst
Iterator tiga argumen ditambahkan di Jawa 9
Thorbjørn Ravn Andersen
3

Saya pribadi, saya akan mempersingkat kelas DoubleSequenceGenerator sedikit untuk barang lainnya dan hanya menggunakan satu metode generator urutan yang berisi opsi untuk memanfaatkan apa pun yang diinginkan presisi yang diinginkan atau tidak menggunakan presisi sama sekali:

Dalam metode generator di bawah ini, jika tidak ada (atau nilai apa pun yang kurang dari 0) dipasok ke parameter setPrecision opsional maka tidak ada pembulatan presisi desimal yang dilakukan. Jika 0 diberikan untuk nilai presisi maka angka dibulatkan ke terdekat mereka seluruh nomor (yaitu: 89,674 dibulatkan ke 90.0). Jika nilai presisi spesifik lebih besar dari 0 diberikan maka nilai dikonversi ke presisi desimal itu.

BigDecimal digunakan di sini untuk ... yah .... presisi:

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 ....
}

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

Dan konsol menampilkan:

[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]
DevilsHnd
sumber
Ide yang menarik, meski saya melihat beberapa masalah. 1. Dengan menambahkan valpada setiap iterasi, Anda mendapatkan kerugian presisi aditif. Untuk urutan yang sangat besar, kesalahan pada beberapa angka terakhir bisa jadi signifikan. 2. Panggilan berulang ke BigDecimal.valueOf()relatif mahal. Anda akan mendapatkan kinerja (dan ketepatan) yang lebih baik dengan mengonversi input menjadi BigDecimal, dan menggunakannya BigDecimaluntuk val. Bahkan, dengan menggunakan doublefor val, Anda tidak benar-benar mendapatkan manfaat presisi dari menggunakan BigDecimalkecuali mungkin dengan pembulatan.
NanoWizard
2

Coba ini.

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

Sini,

int java.math.BigDecimal.scale()

Mengembalikan skala BigDecimal ini. Jika nol atau positif, skala adalah jumlah digit di sebelah kanan titik desimal. Jika negatif, nilai unscaled dari angka dikalikan sepuluh dengan kekuatan negasi skala. Misalnya, skala -3 berarti nilai unscaled dikalikan dengan 1000.

Di main ()

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

Dan Output:

[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]
Maddy
sumber
2
  1. Apakah sudah ada perpustakaan yang menyediakan fungsionalitas semacam ini?

    Maaf, saya tidak tahu, tetapi menilai dari jawaban lain, dan kesederhanaannya - tidak, tidak ada. Tidak dibutuhkan. Hampir saja...

  2. Jika tidak, apakah ada masalah dengan pendekatan saya?

    Iya dan tidak. Anda memiliki setidaknya satu bug, dan beberapa ruang untuk meningkatkan kinerja, tetapi pendekatan itu sendiri sudah benar.

    1. Bug Anda: kesalahan pembulatan (cukup ubah while (mult*fraction < 1.0)ke while (mult*fraction < 10.0)dan yang seharusnya memperbaikinya)
    2. Semua yang lain tidak mencapai end... well, mungkin mereka hanya tidak cukup jeli untuk membaca komentar dalam kode Anda
    3. Yang lainnya lebih lambat.
    4. Hanya dengan mengubah kondisi pada loop utama dari int < Doublemenjadi int < intterasa akan meningkatkan kecepatan kode Anda
  3. Apakah ada yang punya pendekatan yang lebih baik untuk ini?

    Hmm ... Dalam hal apa?

    1. Kesederhanaan? generateSequenceDoubleStreamdari @Evgeniy Khyst terlihat cukup sederhana. Dan harus digunakan ... tapi mungkin tidak, karena dua poin berikutnya
    2. Tepat? generateSequenceDoubleStreamtidak! Tapi tetap bisa diselamatkan dengan polanya start + step*i. Dan start + step*ipolanya tepat. Hanya BigDoubledan aritmatika titik tetap dapat mengalahkannya. Tapi BigDoubles lambat, dan aritmatika titik tetap manual membosankan dan mungkin tidak sesuai untuk data Anda. Ngomong-ngomong, dalam hal ketepatan, Anda dapat menghibur diri dengan ini: https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
    3. Kecepatan ... yah sekarang kita berada di tanah yang goyah. Lihat replika ini https://repl.it/repls/RespectfulSufficientWorker Saya tidak memiliki stan tes yang layak sekarang, jadi saya menggunakan repl.it ... yang sama sekali tidak memadai untuk pengujian kinerja, tetapi itu bukan poin utama. Intinya adalah - tidak ada jawaban yang pasti. Kecuali bahwa mungkin dalam kasus Anda, yang tidak sepenuhnya jelas dari pertanyaan Anda, Anda pasti tidak boleh menggunakan BigDecimal (baca lebih lanjut).

      Saya sudah mencoba bermain dan mengoptimalkan input besar. Dan kode asli Anda, dengan beberapa perubahan kecil - tercepat. Tapi mungkin Anda membutuhkan sejumlah kecil List? Maka itu bisa menjadi cerita yang sama sekali berbeda.

      Kode ini cukup sederhana untuk seleraku, dan cukup cepat:

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

    Jika Anda lebih suka cara yang lebih elegan (atau kami menyebutnya idiomatis), saya pribadi menyarankan:

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

    Bagaimanapun, peningkatan kinerja yang mungkin adalah:

    1. Coba beralih dari Doubleke double, dan jika Anda benar-benar membutuhkannya, Anda dapat beralih kembali, menilai dari tes, masih mungkin lebih cepat. (Tapi jangan percaya, coba sendiri dengan data Anda di lingkungan Anda. Seperti yang saya katakan - repl.it menyebalkan untuk tolok ukur)
    2. Sihir kecil: loop terpisah untuk Math.round()... mungkin ada hubungannya dengan lokalitas data. Saya tidak merekomendasikan ini - hasilnya sangat tidak stabil. Tapi itu menyenangkan.

      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. Anda harus mempertimbangkan untuk menjadi lebih malas dan menghasilkan angka sesuai permintaan tanpa menyimpan kemudian di Lists

  4. Saya menduga bahwa dalam sebagian besar kasus penggunaan umum, overhead pembulatan akan diabaikan.

    Jika Anda mencurigai sesuatu - ujilah :-) Jawaban saya adalah "Ya", tapi sekali lagi ... jangan percaya padaku. Menguji.

Jadi, kembali ke pertanyaan utama: Apakah ada cara yang lebih baik?
Ya tentu saja!
Tapi itu tergantung.

  1. Pilih Desimal Besar jika Anda membutuhkan angka yang sangat besar dan sangat kecil . Tetapi jika Anda melemparkan mereka kembali ke Double, dan lebih dari itu, gunakan dengan jumlah "dekat" - tidak perlu bagi mereka! Periksa balasan yang sama: https://repl.it/repls/RespectfulSufficientWorker - tes terakhir menunjukkan bahwa tidak akan ada perbedaan dalam hasil , tetapi penggalian kehilangan dalam kecepatan.
  2. Buat beberapa optimasi mikro berdasarkan properti data Anda, tugas Anda, dan lingkungan Anda.
  3. Lebih suka kode pendek dan sederhana jika tidak ada banyak keuntungan dari peningkatan kinerja 5-10%. Jangan habiskan waktu Anda
  4. Mungkin gunakan aritmatika titik tetap jika Anda bisa dan jika itu layak.

Selain itu, Anda baik-baik saja.

PS . Ada juga implementasi Formula Kahan Summation di repl ... hanya untuk bersenang-senang. https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346 dan berhasil - Anda dapat mengurangi kesalahan penjumlahan

x00
sumber