Başlangıç, bitiş ve adım verilen bir Liste <Double> değer dizisi oluşturmanın en iyi yolu?


14

Aslında burada bunun cevabını bulamadığım için çok şaşırdım, ama belki de sadece yanlış arama terimlerini falan kullanıyorum. Bulabildiğim en yakın şey bu , ama doublebelirli bir adım büyüklüğüne sahip belirli bir s aralığı üretmeyi soruyorlar ve cevaplar böyle davranıyor. Ben rasgele başlangıç, bitiş ve adım boyutu ile sayıları üretecek bir şeye ihtiyacım var.

Orada anlamaya sahip bir yerde zaten bir kütüphane bu gibi bazı yöntem olmak, ama kolayca bulmak mümkün değildi eğer öyleyse (yine, belki de yanlış arama terimlerini falan kullanıyorum). İşte bunu yapmak için son birkaç dakika içinde kendi başıma pişirdim:

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

}

Bu yöntemler step, dizi dizini ile çarpılan ve startofsete eklenen basit bir döngü çalıştırır . Bu, sürekli artışla ( stepher bir yinelemede bir değişkene ekleme yapmak gibi) meydana gelebilecek birleşik kayan nokta hatalarını azaltır.

generateSequenceRoundedBir kesirli adım büyüklüğünün belirgin kayan nokta hatalarına neden olabileceği durumlar için yöntemi ekledim . Biraz daha aritmetik gerektirir, bu nedenle bizimki gibi son derece performansa duyarlı durumlarda, yuvarlama gereksiz olduğunda daha basit yöntemi kullanma seçeneğine sahip olmak güzeldir. Çoğu genel kullanım durumunda yuvarlama yükünün ihmal edilebilir olacağını düşünüyorum.

Ben kasıtlı gibi "anormal" argümanları işlemek için mantığı hariç geldiğini hatırlatırız Infinity, NaN, start> end, negatif veya stepbasitlik için boyut ve eldeki soru üzerine odaklanmak istiyoruz.

Aşağıda bazı örnek kullanım ve karşılık gelen çıktılar verilmiştir:

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]

Bu tür bir işlevsellik sağlayan mevcut bir kütüphane var mı?

Değilse, yaklaşımımla ilgili herhangi bir sorun var mı?

Herkes bu konuda daha iyi bir yaklaşım var mı?

Yanıtlar:


17

Java 11 Stream API kullanılarak sekanslar kolayca oluşturulabilir.

Basit yaklaşım DoubleStreamşunları kullanmaktır :

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

Çok sayıda yinelemeye sahip aralıklarda, doublehassasiyet hatası birikerek aralığın sonuna daha yakın daha büyük hataya neden olabilir. Hata, IntStreamtamsayılara ve tekli çift çarpanına geçilip kullanılarak en aza indirilebilir :

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

doubleKesin bir hatadan kurtulmak BigDecimaliçin kullanılabilir:

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

Örnekler:

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

Bu imza ile yöntem yinelemesi (3 parametre) Java 9'a eklendi. Böylece, Java 8 için kod şöyle görünür

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

Bu daha iyi bir yaklaşım.
Vishwa Ratna

Birkaç derleme hataları (JDK 1.8.0) görüyorum: 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. Benzer hatalar IntStream.iterateve Stream.iterate. Ayrıca non-static method doubleValue() cannot be referenced from a static context,.
NanoWizard

1
Cevap Java 11 kodunu içerir
Evgeniy Khyst

@NanoWizard, cevabı Java 8
Evgeniy Khyst

Üç argüman yineleyici Java 9
Thorbjørn Ravn Andersen

3

Ben şahsen, DoubleSequenceGenerator sınıfını diğer güzellikler için biraz kısaltır ve istenen hassasiyeti kullanma veya herhangi bir hassasiyet kullanma seçeneğini içeren tek bir dizi üreteci yöntemini kullanırdım:

Aşağıdaki jeneratör yönteminde , isteğe bağlı setPrecision parametresine hiçbir şey (veya 0'dan küçük bir değer ) girilmezse, ondalık duyarlıklı yuvarlama yapılmaz. Bir kesinlik değeri için 0 verilirse , sayılar en yakın tam sayılarına yuvarlanır (yani: 89.674, 90.0'a yuvarlanır). 0'dan büyük belirli bir kesinlik değeri verilirse , değerler o ondalık kesinlik değerine dönüştürülür.

BigDecimal burada ... iyi ... hassasiyet için kullanılır:

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

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

Ve konsol şunları görüntüler:

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

İlginç fikirler, ancak birkaç sorun görüyorum. 1. valHer yinelemeye ekleyerek , hassas hassasiyet kaybı elde edersiniz. Çok büyük diziler için, son birkaç sayıdaki hata önemli olabilir. 2. Tekrarlanan aramalar BigDecimal.valueOf()nispeten pahalıdır. Sen hiç girdileri dönüştürerek daha iyi performans (ve hassasiyet) alırsınız BigDecimals ve kullanma BigDecimaliçin val. Aslında, bir doublefor kullanarak val, BigDecimalbelki de yuvarlama dışında kullanmaktan herhangi bir hassas fayda elde edemezsiniz .
NanoWizard

2

Bunu dene.

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

Buraya,

int java.math.BigDecimal.scale()

Bu BigDecimal'in ölçeğini döndürür. Sıfır veya pozitifse, ölçek ondalık virgülünün sağındaki basamak sayısıdır. Negatifse, sayının ölçeklendirilmemiş değeri, ölçeğin olumsuzlanmasının gücüyle on ile çarpılır. Örneğin, -3 ölçeği ölçeklenmemiş değerin 1000 ile çarpıldığı anlamına gelir.

Ana ()

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

Ve Çıktı:

[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. Bu tür bir işlevsellik sağlayan mevcut bir kütüphane var mı?

    Üzgünüm, bilmiyorum, ama diğer cevaplara ve göreceli sadeliğine bakarak - hayır, yok. Gerek yok. Neredeyse ...

  2. Değilse, yaklaşımımla ilgili herhangi bir sorun var mı?

    Evet ve hayır. En az bir hatanız ve performans artışı için biraz alanınız var, ancak yaklaşımın kendisi doğru.

    1. Sizin hata: yuvarlama hatası (sadece değiştirmek while (mult*fraction < 1.0)için while (mult*fraction < 10.0)ve şimdi düzelmiş olmalı)
    2. Diğerleri ulaşmıyor end... iyi, belki de kodunuzdaki yorumları okuyacak kadar dikkatli değildiler
    3. Diğerleri daha yavaştır.
    4. Sadece ana döngü içinde durumun değiştirilmesine int < Doublekarşı int < intbelirgin kodunuzun hızını artıracak
  3. Herkes bu konuda daha iyi bir yaklaşım var mı?

    Hmm ... Ne şekilde?

    1. Basitlik? generateSequenceDoubleStreamEfgeniy Khyst oldukça basit görünüyor. Ve kullanılmalı ... ama belki hayır, sonraki iki noktadan dolayı
    2. Hassas? generateSequenceDoubleStreamdeğil! Ama yine de desen ile kaydedilebilir start + step*i. Ve start + step*idesen kesindir. Sadece BigDoubleve sabit noktalı aritmetik bunu yenebilir. Ancak BigDoubles yavaştır ve manuel sabit nokta aritmetiği sıkıcıdır ve verileriniz için uygun olmayabilir. Bu arada, hassas konularda, kendinizi şununla eğlendirebilirsiniz: https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
    3. Hız ... şimdi titrek bir zemindeyiz. Bu cevap https://repl.it/repls/RespectfulSicientWorker Şu anda iyi bir test standı yok, bu yüzden repl.it kullandım ... performans testi için tamamen yetersiz, ama ana nokta değil. Mesele şu ki - kesin bir cevap yok. Sorunuzdan tamamen açık olmayan belki de sizin durumunuz dışında, kesinlikle BigDecimal kullanmamalısınız (daha fazla okuyun).

      Büyük girdiler için oynamayı ve optimize etmeyi denedim. Ve orijinal kodunuz, bazı küçük değişikliklerle - en hızlısı. Ama belki de muazzam miktarda küçük Lists'e ihtiyacınız var ? O zaman bu tamamen farklı bir hikaye olabilir.

      Bu kod benim zevkime göre oldukça basit ve yeterince hızlı:

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

    Daha zarif bir yol tercih ediyorsanız (ya da deyimsel demeliyiz), şahsen şunu öneririm:

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

    Her neyse, olası performans artışları:

    1. 'Den geçiş Doubleyapmayı deneyin doubleve gerçekten ihtiyacınız varsa, testlere bakarak tekrar geri dönebilirsiniz, yine de daha hızlı olabilir. (Ama güvenmeyin, ortamınızdaki verilerle kendiniz deneyin. Dediğim gibi - repl.it kriterler için berbat)
    2. Biraz sihir: Math.round()... için ayrı bir döngü ... belki de veri konumuyla ilgisi vardır. Bunu tavsiye etmiyorum - sonuç çok kararsız. Ama eğlenceli.

      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. Kesinlikle daha tembel olmayı düşünmeli ve daha sonra Lists

  4. Çoğu genel kullanım durumunda yuvarlama yükünün ihmal edilebilir olacağını düşünüyorum.

    Bir şeyden şüphelenirseniz - test edin :-) Cevabım "Evet", ama yine ... bana inanma. Dene.

Yani ana soruya dönelim: Daha iyi bir yol var mı?
Evet tabi ki!
Ama buna bağlı.

  1. Çok büyük sayılara ve çok küçük sayılara ihtiyacınız varsa Büyük Ondalık'ı seçin . Ama eğer onları geri koyarsanız ve daha fazlasını yaparsanız , bunu "yakın" büyüklükte rakamlarla kullanın - onlara gerek yok! Aynı yanıt alın: https://repl.it/repls/RespectfulSicientWorker - son test sonuçlarda bir fark olmayacağını , ancak hızda bir kazma kaybının olmayacağını gösterir .Double
  2. Veri özelliklerinize, görevinize ve ortamınıza bağlı olarak bazı mikro optimizasyonlar yapın.
  3. % 5-10'luk performans artışından elde edilecek fazla bir şey yoksa kısa ve basit kodu tercih edin. Zamanını bellendirme
  4. Belki yapabiliyorsanız ve buna değerse sabit noktalı aritmetik kullanın.

Bunun dışında iyisin.

PS . Temsilcide Kahan Sumulation Formula uygulaması da var ... sadece eğlence için. https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346 ve işe yarıyor - sen yapabilirsiniz toplamıdır hatalarını azaltmak

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.