Найкращий спосіб створити список <Double> послідовності значень, заданих початком, кінцем та кроком?


14

Я насправді дуже здивований, що мені не вдалося знайти відповіді на це, хоча, можливо, я просто використовую неправильні пошукові терміни чи щось таке. Найближче, що я міг знайти, це це , але вони запитують про генерування певного діапазону doubles із певним розміром кроку, і відповіді трактують його як таке. Мені потрібно щось, що генерує числа з довільним початком, кінцем та розміром кроку.

Я думаю, що в бібліотеці десь уже має бути такий метод, як цей, але якщо так, я не зміг його легко знайти (знову ж, можливо, я просто використовую неправильні пошукові терміни чи щось таке). Ось ось, що я готував самостійно за останні кілька хвилин, щоб зробити це:

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метод для тих випадків, коли розмір дробового кроку може спричинити помітні помилки з плаваючою комою. Це вимагає трохи більше арифметики, тому в надзвичайно чутливих до ефективності ситуаціях, таких як наша, приємно мати можливість використовувати більш простий метод, коли округлення є непотрібним. Я підозрюю, що в більшості випадків загального використання округлення накладних витрат буде незначним.

Зверніть увагу , що я навмисно виключив логіку для обробки «ненормальні» аргументів , таких як Infinity, NaN, start> 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))

Це кращий підхід.
Вішва Ратна

Я бачу кілька помилок компіляції (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. Подібні помилки для IntStream.iterateта Stream.iterate. Також non-static method doubleValue() cannot be referenced from a static context.
NanoWizard

1
Відповідь містить код Java 11
Євгеній

@NanoWizard продовжив відповідь зразком для Java 8
Євгеній

Третій аргумент ітератор доданий в Java 9
Thorbjørn Ravn Andersen

3

Мене особисто я би скоротив клас DoubleSequenceGenerator на інші смаколики і застосував би лише один метод генератора послідовностей, який містить можливість використовувати будь-яку потрібну точність або взагалі не використовувати точність:

У наведеному нижче способі генератора, якщо додатковому параметру setPrecision не подається нічого (або якесь значення менше 0), тоді не проводиться десятичне округлення точності. Якщо для значення точності подається 0, то числа округляються до їх найближчого цілого числа (тобто: 89.674 округлюється до 90.0). Якщо подано конкретне значення точності, що перевищує 0 , значення перетворюються на десяткову точність.

Тут використовується 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 ....
}

І в основному ():

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 та використовуючи BigDecimalдля val. Насправді, використовуючи doubleдля val, ви насправді не отримуєте жодної переваги від точності від використання, BigDecimalкрім, можливо, з округленням.
NanoWizard

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. Якщо нуль або додатний, шкала - це число цифр праворуч від десяткової коми. Якщо від'ємне значення, незазначене значення числа множиться на десять на потужність заперечення шкали. Наприклад, шкала -3 означає, що немасштабне значення множиться на 1000.

В основному ()

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. Ваша помилка: помилка округлення (просто змінити , while (mult*fraction < 1.0)щоб while (mult*fraction < 10.0)і це повинно виправити)
    2. Всі інші не доходять до end... ну, можливо, вони просто не були достатньо спостережливими, щоб прочитати коментарі у вашому коді
    3. Усі інші повільніше.
    4. Просто зміна умови в основному циклі з int < Doubleна int < intпомітно збільшить швидкість вашого коду
  3. Хтось має кращий підхід до цього?

    Хм ... Яким чином?

    1. Простота? generateSequenceDoubleStream@Evgeniy Khyst виглядає досить просто. І слід використовувати ... але, можливо, ні, через два наступні моменти
    2. Точні? generateSequenceDoubleStreamне! Але все ж можна зберегти з малюнком start + step*i. І start + step*iвізерунок точний. Тільки BigDoubleі фіксованою точкою арифметика може побити його. Але BigDoubleце повільно, а ручна арифметика з фіксованою точкою втомлива і може не відповідати вашим даним. До речі, з питань точності ви можете розважитись цим: https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
    3. Швидкість ... ну зараз ми на хитких місцях. Ознайомтеся з цим https://repl.it/repls/RespectfulSufficientWorker Я зараз не маю гідного тестового стенда, тому я використав repl.it ..., який абсолютно не відповідає тестуванню продуктивності, але це не головний момент. Справа в тому, - однозначної відповіді немає. За винятком того, що, можливо, у вашому випадку, що не зовсім зрозуміло з вашого питання, ви точно не повинні використовувати BigDecimal (читайте далі).

      Я намагався грати та оптимізувати для великих входів. А ваш оригінальний код із деякими незначними змінами - найшвидший. Але, можливо, вам потрібна величезна кількість маленьких Listс? Тоді це може бути зовсім інша історія.

      Цей код досить простий на мій смак і досить швидкий:

        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. Трохи магії: окремий цикл для 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. Виберіть Великі десяткові, якщо вам потрібні дуже великі числа і дуже маленькі числа. Але якщо ви повернете їх до того Doubleчи іншого, використовуйте його з числами "близької" величини - в них немає потреби! Отримуйте той же репл: https://repl.it/repls/RespectfulSufficientWorker - останній тест показує, що різниця в результатах не буде , а втрата копання у швидкості.
  2. Зробіть кілька оптимізацій на основі властивостей даних, завдання та оточення.
  3. Віддайте перевагу короткому та простому коду, якщо від підвищення продуктивності на 5-10% не дуже багато. Не витрачайте часу
  4. Можливо, використовуйте арифметику з фіксованою точкою, якщо можете, і якщо вона того варта.

Крім цього, у вас все добре.

PS . Є також реалізація формули формулювання підсумків Kahan у відповіді ... просто для задоволення. https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346, і це працює - ви можете зменшити помилки підсумовування

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.