web-dev-qa-db-fra.com

Le meilleur moyen de générer une séquence List <Double> de valeurs étant donné le début, la fin et l'étape?

Je suis en fait très surpris de ne pas avoir pu trouver la réponse à cela ici, bien que j'utilise peut-être simplement les mauvais termes de recherche ou quelque chose. Le plus proche que j'ai pu trouver est this , mais ils demandent de générer une plage spécifique de double avec une taille de pas spécifique, et les réponses la traitent comme telle. J'ai besoin de quelque chose qui générera les nombres avec une taille de début, de fin et de pas arbitraire.

Je suppose que doit être une méthode comme celle-ci dans une bibliothèque déjà quelque part, mais si c'est le cas, je n'ai pas pu la trouver facilement (encore une fois, peut-être J'utilise simplement les mauvais termes de recherche ou quelque chose comme ça). Voici donc ce que j'ai préparé par moi-même au cours des dernières minutes pour le faire:

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

}

Ces méthodes exécutent une boucle simple multipliant le step par l'index de séquence et s'ajoutant au décalage start. Cela atténue les erreurs de virgule flottante qui se produiraient avec une incrémentation continue (comme l'ajout de step à une variable à chaque itération).

J'ai ajouté la méthode generateSequenceRounded pour les cas où une taille de pas fractionnaire peut provoquer des erreurs à virgule flottante notables. Cela nécessite un peu plus d'arithmétique, donc dans des situations extrêmement sensibles aux performances comme la nôtre, il est agréable d'avoir la possibilité d'utiliser la méthode plus simple lorsque l'arrondi n'est pas nécessaire. Je soupçonne que dans la plupart des cas d'utilisation générale, les frais généraux d'arrondi seraient négligeables.

Notez que j'ai intentionnellement exclu la logique pour gérer des arguments "anormaux" tels que Infinity, NaN, start> end, ou un négatif step ] taille pour plus de simplicité et désir de se concentrer sur la question posée.

Voici un exemple d'utilisation et la sortie correspondante:

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]

Existe-t-il déjà une bibliothèque qui offre déjà ce type de fonctionnalités?

Sinon, y a-t-il des problèmes avec mon approche?

Quelqu'un at-il une meilleure approche à ce sujet?

14
NanoWizard

Les séquences peuvent être facilement générées en utilisant Java 11 Stream API.

L'approche la plus simple consiste à utiliser 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());
}

Sur les plages avec un grand nombre d'itérations, une erreur de précision double pourrait s'accumuler, entraînant une erreur plus importante plus près de la fin de la plage. L'erreur peut être minimisée en passant à IntStream et en utilisant des entiers et un double multiplicateur simple:

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

Pour se débarrasser d'une erreur de précision double, BigDecimal peut être utilisé:

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

Exemples:

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

La méthode itérer avec cette signature (3 paramètres) a été ajoutée dans Java 9. Donc, pour Java 8 le code ressemble à

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

Personnellement, je raccourcirais un peu la classe DoubleSequenceGenerator pour d'autres goodies et n'utiliserais qu'un seul générateur de séquence méthode qui contient l'option d'utiliser la précision souhaitée souhaitée ou de n'utiliser aucune précision du tout:

Dans la méthode du générateur ci-dessous, si rien (ou une valeur inférieure à 0) n'est fourni au paramètre setPrecision facultatif, aucun arrondi de précision décimale n'est effectué. Si est fourni pour une valeur de précision, les nombres sont arrondis au nombre entier le plus proche (c'est-à-dire que 89,674 est arrondi à 90,0). Si une valeur de précision spécifique supérieure à est fournie, les valeurs sont converties en cette précision décimale.

BigDecimal est utilisé ici pour ... eh bien ... précision:

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

Et dans 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));

Et la console affiche:

[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]
3
DevilsHnd
  1. Existe-t-il déjà une bibliothèque qui offre déjà ce type de fonctionnalités?

    Désolé, je ne sais pas, mais à en juger par les autres réponses et leur relative simplicité - non, il n'y en a pas. Ce n'est pas nécessaire. Enfin, presque ...

  2. Sinon, y a-t-il des problèmes avec mon approche?

    Oui et non. Vous avez au moins un bogue et une certaine marge d'amélioration des performances, mais l'approche elle-même est correcte.

    1. Votre bug: erreur d'arrondi (changez simplement while (mult*fraction < 1.0) en while (mult*fraction < 10.0) et cela devrait le corriger)
    2. Tous les autres n'atteignent pas le end... eh bien, peut-être qu'ils n'étaient pas assez attentifs pour lire les commentaires dans votre code
    3. Tous les autres sont plus lents.
    4. Le simple changement de condition dans la boucle principale de int < Double À int < int Augmentera sensiblement la vitesse de votre code
  3. Quelqu'un at-il une meilleure approche à ce sujet?

    Hmm ... De quelle manière?

    1. Simplicité? generateSequenceDoubleStream de @Evgeniy Khyst semble assez simple. Et devrait être utilisé ... mais peut-être que non, à cause des deux points suivants
    2. Précis? generateSequenceDoubleStream ne l'est pas! Mais il est toujours possible d'enregistrer avec le modèle start + step*i. Et le modèle start + step*i Est précis. Seuls BigDouble et l'arithmétique à virgule fixe peuvent le battre. Mais BigDoubles sont lents et l'arithmétique manuelle à virgule fixe est fastidieuse et peut être inappropriée pour vos données. Soit dit en passant, sur les questions de précision, vous pouvez vous divertir avec ceci: https://docs.Oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
    3. Vitesse ... eh bien maintenant nous sommes sur un terrain fragile. Découvrez cette repl https://repl.it/repls/RespectfulSufficientWorker Je n'ai pas de banc d'essai décent en ce moment, j'ai donc utilisé repl.it ... qui est totalement inadéquat pour les tests de performances, mais ce n'est pas le point principal. Le fait est - il n'y a pas de réponse définitive. Sauf que peut-être dans votre cas, ce qui ne ressort pas totalement de votre question, vous ne devriez certainement pas utiliser BigDecimal (lire plus loin).

      J'ai essayé de jouer et d'optimiser pour les grosses entrées. Et votre code d'origine, avec quelques modifications mineures - le plus rapide. Mais peut-être avez-vous besoin d'énormes quantités de petits Lists? Cela peut alors être une histoire totalement différente.

      Ce code est assez simple à mon goût, et assez rapide:

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

    Si vous préférez une manière plus élégante (ou nous devrions l'appeler idiomatique), je suggère personnellement:

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

    Quoi qu'il en soit, les améliorations possibles des performances sont les suivantes:

    1. Essayez de passer de Double à double, et si vous en avez vraiment besoin, vous pouvez revenir en arrière, à en juger par les tests, cela peut encore être plus rapide. (Mais ne vous fiez pas à moi, essayez-le vous-même avec vos données dans votre environnement. Comme je l'ai dit - repl.it craint pour les benchmarks)
    2. Un peu de magie: boucle séparée pour Math.round()... peut-être que cela a quelque chose à voir avec la localisation des données. Je ne le recommande pas - le résultat est très instable. Mais c'est amusant.

      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. Vous devriez certainement considérer être plus paresseux et générer des nombres à la demande sans les stocker ensuite dans Lists

  4. Je soupçonne que dans la plupart des cas d'utilisation générale, les frais généraux d'arrondi seraient négligeables.

    Si vous soupçonnez quelque chose - testez-le :-) Ma réponse est "Oui", mais encore une fois ... ne me croyez pas. Essaye-le.

Donc, revenons à la question principale: existe-t-il une meilleure façon?
Oui bien sûr!
Mais cela dépend.

  1. Choisissez Grand Décimal si vous avez besoin de très grands nombres et très petits nombres . Mais si vous les restituez à Double, et plus encore, utilisez-le avec des nombres de magnitude "proche" - pas besoin d'eux! Vérifiez la même réponse: https://repl.it/repls/RespectfulSufficientWorker - le dernier test montre qu'il n'y aura aucune différence dans les résultats , mais une perte de vitesse Dig.
  2. Faites des micro-optimisations en fonction de vos propriétés de données, de votre tâche et de votre environnement.
  3. Préférez un code court et simple s'il n'y a pas grand-chose à gagner d'une amélioration des performances de 5 à 10%. Ne perdez pas votre temps
  4. Utilisez peut-être l'arithmétique à virgule fixe si vous le pouvez et si cela en vaut la peine.

A part ça, tu vas bien.

[~ # ~] ps [~ # ~] . Il y a aussi une implémentation Kahan Summation Formula dans la repl ... juste pour le plaisir. https://docs.Oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346 et cela fonctionne - vous pouvez atténuer les erreurs de sommation

2
x00

Essaye ça.

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

Ici,

int Java.math.BigDecimal.scale()

Renvoie l'échelle de ce BigDecimal. Si zéro ou positif, l'échelle est le nombre de chiffres à droite de la virgule décimale. S'il est négatif, la valeur non mise à l'échelle du nombre est multipliée par dix à la puissance de la négation de l'échelle. Par exemple, une échelle de -3 signifie que la valeur non mise à l'échelle est multipliée par 1000.

En principal ()

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

Et sortie:

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