A melhor maneira de gerar uma sequência List <Double> de valores dados início, fim e etapa?


14

Estou realmente muito surpreso por não ter encontrado a resposta para isso aqui, embora talvez esteja apenas usando os termos de pesquisa incorretos ou algo assim. O mais próximo que pude encontrar é esse , mas eles perguntam sobre a geração de um intervalo específico de doubles com um tamanho de etapa específico e as respostas o tratam como tal. Preciso de algo que gere os números com tamanho arbitrário de início, fim e etapa.

Eu acho que deve haver algum método como esse em uma biblioteca em algum lugar, mas se assim não for possível encontrá-lo facilmente (novamente, talvez eu esteja apenas usando os termos de pesquisa incorretos ou algo assim). Então, aqui está o que eu cozinhei por conta própria nos últimos minutos para fazer isso:

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

}

Esses métodos executam um loop simples multiplicando steppelo índice de sequência e adicionando ao startdeslocamento. Isso reduz os erros de composição de ponto flutuante que ocorreriam com incrementos contínuos (como adicionar a stepuma variável em cada iteração).

Adicionei o generateSequenceRoundedmétodo para os casos em que um tamanho de etapa fracionário pode causar erros perceptíveis de ponto flutuante. Requer um pouco mais de aritmética, portanto, em situações extremamente sensíveis ao desempenho como a nossa, é bom ter a opção de usar o método mais simples quando o arredondamento é desnecessário. Eu suspeito que, na maioria dos casos de uso geral, a sobrecarga de arredondamento seria insignificante.

Nota que eu intencionalmente excluídos lógica para lidar com argumentos "anormais", como Infinity, NaN, start> end, ou um negativo steptamanho para simplicidade e desejam focar a questão em apreço.

Aqui estão alguns exemplos de uso e saída correspondente:

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 uma biblioteca existente que já fornece esse tipo de funcionalidade?

Caso contrário, há algum problema com minha abordagem?

Alguém tem uma abordagem melhor para isso?

Respostas:


17

As seqüências podem ser facilmente geradas usando a API Java 11 Stream.

A abordagem direta é usar 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());
}

Em intervalos com um grande número de iterações, um doubleerro de precisão pode se acumular, resultando em um erro maior próximo ao final do intervalo. O erro pode ser minimizado alternando para IntStreame usando números inteiros e multiplicador duplo único:

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

Para se livrar de um doubleerro de precisão, BigDecimalpode ser usado:

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

Exemplos:

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

O método iterar com esta assinatura (3 parâmetros) foi adicionado no Java 9. Portanto, para o Java 8, o código parece

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

Essa é uma abordagem melhor.
Vishwa Ratna

Eu estou vendo vários erros de compilação (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. Erros semelhantes para IntStream.iteratee Stream.iterate. Além disso non-static method doubleValue() cannot be referenced from a static context,.
NanoWizard 31/12/19

11
A resposta contém código Java 11
Evgeniy Khyst

O @NanoWizard estendeu a resposta com uma amostra para Java 8
Evgeniy Khyst

O iterador de três argumentos foi adicionado no Java 9
Thorbjørn Ravn Andersen

3

Pessoalmente, reduziria um pouco a classe DoubleSequenceGenerator para outros itens e usaria apenas um método gerador de sequência que contém a opção de utilizar a precisão desejada ou não usar precisão:

No método do gerador abaixo, se nada (ou qualquer valor menor que 0) for fornecido ao parâmetro opcional setPrecision , nenhum arredondamento de precisão decimal será realizado. Se 0 for fornecido para um valor de precisão, os números serão arredondados para o número inteiro mais próximo (ou seja: 89,674 é arredondado para 90,0). Se um valor específico de precisão maior que 0 for fornecido, os valores serão convertidos para essa precisão decimal.

BigDecimal é usado aqui para ... bem ... precisão:

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 em 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 o console exibe:

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

Idéias interessantes, embora eu veja algumas questões. 1. Ao adicionar a valcada iteração, você obtém perda de precisão aditiva. Para seqüências muito grandes, o erro nos últimos números pode ser significativo. 2. Chamadas repetidas para BigDecimal.valueOf()são relativamente caras. Você obterá melhor desempenho (e precisão) convertendo as entradas em BigDecimals e usando BigDecimalfor val. De fato, usando a doublepara val, você não está obtendo nenhum benefício de precisão, BigDecimalexceto talvez com o arredondamento.
NanoWizard 23/12/19

2

Tente isso.

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

Aqui,

int java.math.BigDecimal.scale()

Retorna a escala deste BigDecimal. Se zero ou positivo, a escala é o número de dígitos à direita do ponto decimal. Se negativo, o valor não escalado do número é multiplicado por dez à potência da negação da escala. Por exemplo, uma escala de -3 significa que o valor não dimensionado é multiplicado por 1000.

Principal ()

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

E saída:

[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. Existe uma biblioteca existente que já fornece esse tipo de funcionalidade?

    Desculpe, eu não sei, mas a julgar por outras respostas, e sua relativa simplicidade - não, não há. Não há necessidade. Bem, quase...

  2. Caso contrário, há algum problema com minha abordagem?

    Sim e não. Você tem pelo menos um bug e espaço para aumentar o desempenho, mas a abordagem em si está correta.

    1. Seu erro: erro de arredondamento (basta mudar while (mult*fraction < 1.0)para while (mult*fraction < 10.0)e isso deve corrigi-lo)
    2. Todos os outros não alcançam o end... bem, talvez eles simplesmente não estivessem atentos o suficiente para ler comentários em seu código
    3. Todos os outros são mais lentos.
    4. Apenas alterar a condição no loop principal de int < Doublepara int < intaumentará notavelmente a velocidade do seu código
  3. Alguém tem uma abordagem melhor para isso?

    Hmm ... de que maneira?

    1. Simplicidade? generateSequenceDoubleStreamde @Evgeniy Khyst parece bastante simples. E deve ser usado ... mas talvez não, por causa dos próximos dois pontos
    2. Preciso? generateSequenceDoubleStreamnão é! Mas ainda pode ser salvo com o padrão start + step*i. E o start + step*ipadrão é preciso. Somente a BigDoublearitmética de ponto fixo pode vencê-lo. Mas BigDoubles são lentos e a aritmética manual de ponto fixo é entediante e pode ser inadequada para seus dados. A propósito, sobre questões de precisão, você pode se divertir com isso: https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
    3. Velocidade ... bem, agora estamos em terreno instável. Confira esta repl https://repl.it/repls/RespectfulSufficientWorker Não tenho uma bancada de teste decente agora, então usei repl.it ... que é totalmente inadequado para testes de desempenho, mas não é o ponto principal. O ponto é - não há resposta definitiva. Exceto que, talvez no seu caso, o que não está totalmente claro em sua pergunta, você definitivamente não deve usar o BigDecimal (leia mais).

      Eu tentei jogar e otimizar para grandes entradas. E seu código original, com algumas pequenas alterações - a mais rápida. Mas talvez você precise de enormes quantidades de pequenos Lists? Então isso pode ser uma história totalmente diferente.

      Este código é bastante simples para o meu gosto e rápido o suficiente:

        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 você preferir uma maneira mais elegante (ou deveríamos chamá-la de idiomática), eu pessoalmente sugeriria:

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

    De qualquer forma, os possíveis aumentos de desempenho são:

    1. Tente mudar de Doublepara doublee, se você realmente precisar deles, pode voltar novamente, a julgar pelos testes, ainda pode ser mais rápido. (Mas não confie no meu, tente você mesmo com seus dados em seu ambiente. Como eu disse - repl.it é péssimo para benchmarks)
    2. Um pouco de mágica: loop separado para Math.round()... talvez tenha algo a ver com a localidade dos dados. Eu não recomendo isso - o resultado é muito instável. Mas é divertido.

      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. Você definitivamente deve considerar ser mais preguiçoso e gerar números sob demanda sem armazenar em seguida em Lists

  4. Eu suspeito que, na maioria dos casos de uso geral, a sobrecarga de arredondamento seria insignificante.

    Se você suspeitar de algo - teste :-) Minha resposta é "Sim", mas novamente ... não acredite em mim. Teste-o.

Então, voltando à questão principal: existe uma maneira melhor?
Sim, claro!
Mas isso depende.

  1. Escolha Decimal grande se precisar de números muito grandes e números muito pequenos . Mas se você os lançar de volta Doublee mais do que isso, use-o com números de magnitude "próxima" - sem necessidade deles! Faça o checkout da mesma repl: https://repl.it/repls/RespectfulSufficientWorker - o último teste mostra que não haverá diferença nos resultados , mas uma perda de velocidade na escavação.
  2. Faça algumas micro-otimizações com base em suas propriedades de dados, sua tarefa e seu ambiente.
  3. Prefira códigos curtos e simples se não houver muito a ganhar com o aumento de desempenho de 5 a 10%. Não gaste seu tempo
  4. Talvez use aritmética de ponto fixo, se puder e se valer a pena.

Fora isso, você está bem.

PS . Há também uma implementação da Kahan Summation Formula no repl ... apenas por diversão. https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346 e funciona - você pode reduzir os erros de soma

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.