Il modo migliore per generare una sequenza <Double> di valori di valori dato inizio, fine e passaggio?


14

In realtà sono molto sorpreso di non essere riuscito a trovare la risposta qui, anche se forse sto solo usando termini di ricerca sbagliati o qualcosa del genere. Il più vicino che ho trovato è questo , ma mi chiedono di generare un intervallo specifico di doubles con una specifica dimensione del passo e le risposte lo trattano come tale. Ho bisogno di qualcosa che generi i numeri con inizio, fine e dimensione del passo arbitrari.

Immagino che ci debba essere un metodo come questo in una libreria già da qualche parte, ma in tal caso non sono stato in grado di trovarlo facilmente (di nuovo, forse sto solo usando i termini di ricerca sbagliati o qualcosa del genere). Quindi, ecco cosa ho preparato da solo negli ultimi minuti per fare questo:

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

}

Questi metodi eseguono un semplice ciclo moltiplicando stepper l'indice di sequenza e aggiungendolo startall'offset. Ciò mitiga gli errori di virgola mobile che si verificano con un'incremento continuo (come l'aggiunta stepdi una variabile ad ogni iterazione).

Ho aggiunto il generateSequenceRoundedmetodo per quei casi in cui una dimensione del gradino frazionaria può causare notevoli errori in virgola mobile. Richiede un po 'più di aritmetica, quindi in situazioni estremamente sensibili alle prestazioni come la nostra, è bello avere la possibilità di utilizzare il metodo più semplice quando l'arrotondamento non è necessario. Ho il sospetto che nella maggior parte dei casi di utilizzo generale l'arrotondamento sarebbe trascurabile.

Si noti che ho volutamente escluso la logica per la gestione di argomenti "anomale", come Infinity, NaN, start> end, o un negativo stepdimensione per la semplicità e il desiderio di concentrarsi sulla questione a portata di mano.

Ecco alcuni esempi di utilizzo e output corrispondente:

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]

Esiste una libreria esistente che fornisce già questo tipo di funzionalità?

In caso contrario, ci sono problemi con il mio approccio?

Qualcuno ha un approccio migliore a questo?

Risposte:


17

Le sequenze possono essere facilmente generate utilizzando l'API Stream di Java 11.

L'approccio semplice è usare 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());
}

Su intervalli con un numero elevato di iterazioni, si doublepotrebbe accumulare un errore di precisione con conseguente errore più grande vicino alla fine dell'intervallo. L'errore può essere ridotto al minimo passando a IntStreame utilizzando numeri interi e un singolo doppio moltiplicatore:

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

Per sbarazzarsi di un doubleerrore di precisione, è BigDecimalpossibile utilizzare:

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

Esempi:

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

Il metodo iterato con questa firma (3 parametri) è stato aggiunto in Java 9. Quindi, per Java 8 il codice è simile

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

Questo è un approccio migliore.
Vishwa Ratna,

Sto vedendo parecchi errori di compilazione (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. Errori simili per IntStream.iteratee Stream.iterate. Inoltre, non-static method doubleValue() cannot be referenced from a static context.
NanoWizard,

1
La risposta contiene il codice Java 11
Evgeniy Khyst

@NanoWizard ha esteso la risposta con un campione per Java 8
Evgeniy Khyst,

L'iteratore a tre argomenti è stato aggiunto in Java 9
Thorbjørn Ravn Andersen il

3

Personalmente, accorcerei un po 'la classe DoubleSequenceGenerator per altre chicche e utilizzerei solo un metodo generatore di sequenza che contiene l'opzione per utilizzare la precisione desiderata o non usare alcuna precisione:

Nel metodo del generatore di seguito, se non viene fornito nulla (o qualsiasi valore inferiore a 0) al parametro setPrecision opzionale , non viene eseguito alcun arrotondamento di precisione decimale. Se viene fornito 0 per un valore di precisione, i numeri vengono arrotondati al numero intero più vicino (ovvero: 89.674 viene arrotondato a 90.0). Se viene fornito un valore di precisione specifico maggiore di 0, i valori vengono convertiti in quella precisione decimale.

BigDecimal è usato qui per ... beh .... precisione:

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

E in 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));

E la console visualizza:

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

Idee interessanti, anche se vedo alcuni problemi. 1. Aggiungendo a valogni iterazione, si ottiene una perdita di precisione aggiuntiva. Per sequenze molto grandi, l'errore sugli ultimi numeri potrebbe essere significativo. 2. Le chiamate ripetute a BigDecimal.valueOf()sono relativamente costose. Otterrai prestazioni migliori (e precisione) convertendo gli input in BigDecimals e usando BigDecimalper val. In effetti, usando un doublefor val, non si ottiene davvero alcun vantaggio di precisione dall'uso BigDecimalse non forse con l'arrotondamento.
NanoWizard il

2

Prova questo.

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

Qui,

int java.math.BigDecimal.scale()

Restituisce la scala di questo BigDecimal. Se zero o positivo, la scala è il numero di cifre a destra del punto decimale. Se negativo, il valore non scalato del numero viene moltiplicato per dieci per la potenza della negazione della scala. Ad esempio, una scala di -3 indica che il valore non scalato viene moltiplicato per 1000.

In main ()

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

E uscita:

[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. Esiste una libreria esistente che fornisce già questo tipo di funzionalità?

    Scusa, non lo so, ma a giudicare dalle altre risposte e dalla loro relativa semplicità - no, non c'è. Non c'è bisogno. Be 'quasi...

  2. In caso contrario, ci sono problemi con il mio approccio?

    Sì e no. Hai almeno un bug e un po 'di spazio per aumentare le prestazioni, ma l'approccio stesso è corretto.

    1. Il tuo errore: errore di arrotondamento (basta cambiare while (mult*fraction < 1.0)per while (mult*fraction < 10.0)e che dovrebbe risolvere il problema)
    2. Tutti gli altri non raggiungono il end... beh, forse non erano abbastanza osservatori per leggere i commenti nel tuo codice
    3. Tutti gli altri sono più lenti.
    4. Il solo cambiamento delle condizioni nel loop principale da int < Doublea int < intaumenta notevolmente la velocità del codice
  3. Qualcuno ha un approccio migliore a questo?

    Hmm ... In che modo?

    1. Semplicità? generateSequenceDoubleStreamdi @Evgeniy Khyst sembra abbastanza semplice. E dovrebbe essere usato ... ma forse no, a causa dei prossimi due punti
    2. Precise? generateSequenceDoubleStreamnon è! Ma può ancora essere salvato con il modello start + step*i. E il start + step*imodello è preciso. Solo l' BigDoublearitmetica a punto fisso può batterlo. Ma BigDoublei messaggi sono lenti e l'aritmetica manuale a virgola fissa è noiosa e potrebbe non essere appropriata per i tuoi dati. A proposito, per quanto riguarda la precisione, puoi divertirti con questo: https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
    3. Velocità ... beh, ora siamo su terreni traballanti. Dai un'occhiata a questo sostituto https://repl.it/repls/RespectfulSufficientWorker Non ho un banco di prova decente in questo momento, quindi ho usato repl.it ... che è totalmente inadeguato per i test delle prestazioni, ma non è il punto principale. Il punto è che non esiste una risposta definitiva. Tranne che forse nel tuo caso, il che non è del tutto chiaro dalla tua domanda, non dovresti assolutamente usare BigDecimal (leggi oltre).

      Ho provato a giocare e ottimizzare per grandi input. E il tuo codice originale, con alcune piccole modifiche: il più veloce. Ma forse hai bisogno di enormi quantità di piccole Lists? Quindi quella può essere una storia completamente diversa.

      Questo codice è abbastanza semplice per i miei gusti e abbastanza veloce:

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

    Se preferisci un modo più elegante (o dovremmo chiamarlo idiomatico), io personalmente suggerirei:

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

    Ad ogni modo, i possibili miglioramenti delle prestazioni sono:

    1. Prova a passare da Doublea double, e se ne hai davvero bisogno, puoi tornare di nuovo, a giudicare dai test, potrebbe essere ancora più veloce. (Ma non fidarti di me, provalo tu stesso con i tuoi dati nel tuo ambiente. Come ho detto - repl.it fa schifo per i benchmark)
    2. Un po 'di magia: loop separato per Math.round()... forse ha qualcosa a che fare con la localizzazione dei dati. Non lo consiglio - il risultato è molto instabile. Ma è divertente.

      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. Dovresti assolutamente considerare di essere più pigro e generare numeri su richiesta senza archiviarli in Lists

  4. Ho il sospetto che nella maggior parte dei casi di utilizzo generale l'arrotondamento sarebbe trascurabile.

    Se sospetti qualcosa, provalo :-) La mia risposta è "Sì", ma di nuovo ... non credermi. Provalo.

Quindi, tornando alla domanda principale: esiste un modo migliore?
Sì, naturalmente!
Ma dipende.

  1. Scegli Decimale grande se hai bisogno di numeri molto grandi e numeri molto piccoli . Ma se li riporti a Double, e oltre, usalo con numeri di grandezza "ravvicinata" - non c'è bisogno di loro! Controlla lo stesso sostituto: https://repl.it/repls/RespectfulSufficientWorker - l'ultimo test mostra che non ci saranno differenze nei risultati , ma una perdita di scavo nella velocità.
  2. Effettua alcune micro-ottimizzazioni basate sulle proprietà dei tuoi dati, sulla tua attività e sul tuo ambiente.
  3. Preferisci un codice breve e semplice se non c'è molto da guadagnare da un aumento delle prestazioni del 5-10%. Non perdere tempo
  4. Forse puoi usare l'aritmetica a virgola fissa se puoi e se ne vale la pena.

A parte questo, stai bene.

PS . C'è anche un'implementazione di Kahan Summation Formula nella sostituzione ... solo per divertimento. https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346 e funziona: puoi mitigare gli errori di somma

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.