Лучший способ создать последовательность значений List <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]

Существует ли уже существующая библиотека, которая уже обеспечивает такую ​​функциональность?

Если нет, есть ли проблемы с моим подходом?

У кого-нибудь есть лучший подход к этому?

NanoWizard
источник

Ответы:

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
Торбьерн Равн Андерсен
3

Лично я бы немного укоротил класс DoubleSequenceGenerator для других полезностей и использовал бы только один метод генератора последовательностей, который содержит опцию для использования любой желаемой точности или вообще никакой точности:

В приведенном ниже методе генератора, если ничего не указано (или любое значение меньше 0) для необязательного параметра setPrecision, округление с десятичной точностью не выполняется. Если для значения точности задано 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]
DevilsHnd
источник
Интересные идеи, хотя я вижу несколько вопросов. 1. Добавляя к valкаждой итерации, вы получаете аддитивные потери точности. Для очень больших последовательностей ошибка в последних нескольких числах может быть значительной. 2. Повторные звонки BigDecimal.valueOf()относительно дороги. Вы получите лучшую производительность (и точность), преобразовав входные данные в BigDecimals и используя BigDecimalfor val. На самом деле, используя doublefor 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Евгения Хиста выглядит довольно просто. И следует использовать ... но, возможно, нет, из-за следующих двух пунктов
    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. Вы определенно должны рассмотреть более ленивыми и генерировать номера по требованию без сохранения , то в Listс

  4. Я подозреваю, что в большинстве случаев общего использования издержки округления будут незначительными.

    Если вы что-то подозреваете - проверьте это :-) Мой ответ "Да", но опять же ... не верьте мне. Попробуй это.

Итак, вернемся к основному вопросу: есть ли лучший путь?
Ну конечно; естественно!
Но это зависит.

  1. Выберите Большой десятичный, если вам нужны очень большие числа и очень маленькие числа. Но если вы приведете их обратно Double, и более того, используйте это с числами «близкой» величины - в них нет необходимости! Оформить заказ тот же репл: https://repl.it/repls/RespectfulSufficientWorker - последний тест показывает, что не будет никакой разницы в результатах , но потеря скорости раскопок.
  2. Сделайте некоторые микрооптимизации на основе ваших свойств данных, вашей задачи и вашей среды.
  3. Предпочитайте короткий и простой код, если выигрыш от повышения производительности на 5-10% невелик. Не трать свое время
  4. Возможно, используйте арифметику с фиксированной запятой, если можете, и если это того стоит.

Кроме этого, ты в порядке.

PS . В реплик есть также реализация формулы суммирования Кахана ... просто для удовольствия. https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346 и это работает - вы можете уменьшить ошибки суммирования

x00
источник