시작, 종료 및 단계가 주어진 List <Double> 값 시퀀스를 생성하는 가장 좋은 방법은 무엇입니까?


14

나는 실제로 여기에 대한 답변을 찾을 수 없다는 것에 놀랐습니다.하지만 잘못된 검색어 나 무언가를 사용하고있을 수도 있습니다. 내가 찾을 수있는 가장 가까운 것은 this 이지만 double특정 단계 크기의 특정 범위를 생성하는 것에 대해 묻고 답변은 그것을 그렇게 취급합니다. 임의의 시작, 끝 및 단계 크기로 숫자를 생성하는 것이 필요합니다.

나는이 그림 어딘가에 이미 도서관에서이 같은 몇 가지 방법으로,하지만 쉽게 찾을 수 없습니다 만약 그렇다면 (다시, 아마 난 그냥 잘못된 검색어 또는 무언가를 사용하고 있습니다). 마지막 몇 분 동안 직접 요리 한 내용은 다음과 같습니다.

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

이것이 더 나은 접근 방법입니다.
Vishwa Ratna

몇 가지 컴파일 오류 (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.iterateStream.iterate. 또한 non-static method doubleValue() cannot be referenced from a static context.
NanoWizard

1
답변은 Java 11 코드를 포함합니다
Evgeniy Khyst

@NanoWizard 자바 8 샘플로 답을 확장
에브 게니 Khyst

3 개의 인수 반복자가 Java 9에 추가되었습니다
Thorbjørn Ravn Andersen

3

개인적으로 DoubleSequenceGenerator 클래스를 단축하고 다른 장점 을 위해 원하는 정밀도를 사용하거나 정밀도를 전혀 사용하지 않는 옵션을 포함하는 하나의 시퀀스 생성기 방법 만 사용 합니다.

아래 생성기 방법 에서 선택적 setPrecision 매개 변수에 아무 것도 (또는 0 보다 작은 값 ) 제공 되지 않으면 소수 정밀도 반올림이 수행되지 않습니다. 경우 0 다음 정밀 값에 공급되는 숫자가 가장 가까운 반올림됩니다 전체 번호 (예 : 89.674은 90.0로 반올림됩니다). 0보다 큰 특정 정밀도 값 이 제공 되면 값은 해당 10 진수 정밀도로 변환됩니다.

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

그리고 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));

그리고 콘솔은 다음을 표시합니다 :

[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각 반복에 가산하면 가산 정밀도 손실이 발생합니다. 매우 큰 시퀀스의 경우 마지막 몇 숫자의 오류가 중요 할 수 있습니다. 에 대한 반복 호출 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의 스케일을 돌려줍니다. 0 또는 양수인 경우 배율은 소수점 오른쪽의 자릿수입니다. 음수 인 경우 스케일링되지 않은 숫자 값에 스케일 부정의 거듭 제곱에 10을 곱합니다. 예를 들어, 스케일 -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 < Doubleint < 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. 속도 ... 글쎄 지금 우리는 흔들리는 땅에 있습니다. 확인이 REPL https://repl.it/repls/RespectfulSufficientWorker I 성능 테스트를위한 완전히 부적절하다 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. 매우 숫자 매우 작은 숫자 가 필요한 경우 십진수를 선택하십시오 . 그러나을 (를) 다시 캐스팅 하고 그 이상으로 캐스팅하는 경우 "가까운"크기의 숫자와 함께 사용하면됩니다. 동일한 repl을 확인하십시오 : https://repl.it/repls/RespectfulSufficientWorker- 마지막 테스트는 결과에 차이는 없지만 발굴 손실 이 있음을 보여줍니다 .Double
  2. 데이터 속성, 작업 및 환경을 기반으로 미세 최적화를 수행하십시오.
  3. 5-10 %의 성능 향상으로 얻을 것이 많지 않으면 짧고 간단한 코드를 선호하십시오. 시간을 허비하지 마십시오
  4. 가능하고 가치가 있다면 고정 소수점 산술을 사용하십시오.

그 외에는 괜찮습니다.

추신 . 또한 재미를 위해 Kahan Summation Formula 구현이 있습니다. https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346 그리고 작동합니다- 합산 오류를 줄일 있습니다

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.