วิธีที่ดีที่สุดในการสร้างลำดับ List <Double> ของค่าที่กำหนดตั้งแต่เริ่มต้นสิ้นสุดและขั้นตอนคืออะไร


14

จริง ๆ แล้วฉันประหลาดใจมากฉันไม่สามารถหาคำตอบได้ที่นี่ แต่บางทีฉันแค่ใช้คำค้นหาที่ผิดหรืออะไรบางอย่าง ที่ใกล้เคียงที่สุดที่ฉันสามารถหาได้คือสิ่งนี้แต่พวกเขาถามเกี่ยวกับการสร้างช่วงที่เฉพาะเจาะจง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 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 length1.8.0): ข้อผิดพลาดที่คล้ายกันและIntStream.iterate Stream.iterateนอกจากนี้non-static method doubleValue() cannot be referenced from a static context.
NanoWizard

1
คำตอบมีรหัส Java 11
Evgeniy Khyst

@NanoWizard ขยายคำตอบด้วยตัวอย่างสำหรับ Java 8
Evgeniy Khyst

ตัววนซ้ำสามอาร์กิวเมนต์ถูกเพิ่มใน Java 9
Thorbjørn Ravn Andersen

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

และใน 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ในการทำซ้ำแต่ละครั้งคุณจะได้รับการสูญเสียความแม่นยำที่เพิ่มขึ้น สำหรับลำดับที่มีขนาดใหญ่มากข้อผิดพลาดของตัวเลขสองสามตัวสุดท้ายอาจมีนัยสำคัญ 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 หมายถึงค่าที่ไม่ปรับสัดส่วนจะถูกคูณด้วย 1,000

ในหลัก ()

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เลขคณิตเท่านั้นและจุดคงที่สามารถเอาชนะได้ แต่BigDoubles ช้าและการคำนวณเลขคงที่ด้วยตนเองนั้นน่าเบื่อและอาจไม่เหมาะสมสำหรับข้อมูลของคุณ ในเรื่องของความแม่นยำคุณสามารถสร้างความบันเทิงให้ตัวคุณเองด้วยวิธีนี้: 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. อาจใช้เลขคณิตจุดคงที่หากคุณสามารถและถ้ามันคุ้มค่า

นอกจากนั้นคุณก็สบายดี

ปล . นอกจากนี้ยังมีการใช้สูตรสรุปของ Kahan ในแบบจำลอง ... เพื่อความสนุกสนาน https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346และใช้งานได้ - คุณสามารถลดข้อผิดพลาดของการรวมได้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.