Cách tốt nhất để tạo một danh sách <Double> chuỗi các giá trị đã cho bắt đầu, kết thúc và bước?


14

Tôi thực sự rất ngạc nhiên vì tôi không thể tìm thấy câu trả lời cho vấn đề này ở đây, mặc dù có lẽ tôi chỉ đang sử dụng các thuật ngữ tìm kiếm sai hoặc một cái gì đó. Gần nhất tôi có thể tìm thấy là cái này , nhưng họ hỏi về việc tạo ra một phạm vi cụ thể doublevới kích thước bước cụ thể và câu trả lời xử lý nó như vậy. Tôi cần một cái gì đó sẽ tạo ra các số với kích thước bắt đầu, kết thúc và bước tùy ý.

Tôi hình dung có được một số phương pháp như thế này trong một thư viện ở đâu đó rồi, nhưng nếu vì vậy tôi đã không thể tìm thấy nó dễ dàng (một lần nữa, có lẽ tôi là chỉ sử dụng các thuật ngữ tìm kiếm sai hoặc một cái gì đó). Vì vậy, đây là những gì tôi đã tự nấu chín trong vài phút qua để làm điều này:

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

}

Các phương thức này chạy một vòng lặp đơn giản nhân với stepchỉ số chuỗi và thêm vào phần startbù. Điều này giảm thiểu các lỗi dấu phẩy động sẽ xảy ra với sự gia tăng liên tục (chẳng hạn như thêmstep một biến vào mỗi lần lặp).

Tôi đã thêm generateSequenceRoundedphương thức cho những trường hợp kích thước bước phân đoạn có thể gây ra lỗi dấu phẩy động đáng chú ý. Nó đòi hỏi số học nhiều hơn một chút, vì vậy trong các tình huống cực kỳ nhạy cảm về hiệu năng như của chúng tôi, thật tuyệt khi có tùy chọn sử dụng phương pháp đơn giản hơn khi làm tròn là không cần thiết. Tôi nghi ngờ rằng trong hầu hết các trường hợp sử dụng chung, chi phí làm tròn sẽ không đáng kể.

Lưu ý rằng tôi cố tình loại trừ logic để xử lý các đối số "bất thường" Infinity, như NaN, start> endhoặc phủ địnhstep kích thước cho sự đơn giản và mong muốn tập trung vào các câu hỏi trong tầm tay.

Dưới đây là một số ví dụ sử dụng và đầu ra tương ứng:

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]

Có một thư viện hiện có cung cấp loại chức năng này chưa?

Nếu không, có bất kỳ vấn đề với cách tiếp cận của tôi?

Có ai có một cách tiếp cận tốt hơn cho điều này?

Câu trả lời:


17

Các chuỗi có thể được tạo dễ dàng bằng API Stream Java 11.

Cách tiếp cận đơn giản là sử dụng 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());
}

Trên các phạm vi có số lần lặp lớn, doublelỗi chính xác có thể tích lũy dẫn đến lỗi lớn hơn ở gần cuối phạm vi. Lỗi có thể được giảm thiểu bằng cách chuyển sang IntStreamvà sử dụng số nguyên và hệ số nhân đơn:

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

Để loại bỏ một doublelỗi chính xác, BigDecimalcó thể được sử dụng:

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

Ví dụ:

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

Phương thức lặp với chữ ký này (3 tham số) đã được thêm vào Java 9. Vì vậy, đối với Java 8, mã trông giống như

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

Đây là cách tiếp cận tốt hơn.
Vishwa Ratna

Tôi đang thấy một số lỗi biên dịch (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. Lỗi tương tự cho IntStream.iterateStream.iterate. Ngoài ra, non-static method doubleValue() cannot be referenced from a static context.
NanoWizard

1
Câu trả lời chứa mã Java 11
Evgeniy Khyst

@NanoWizard đã mở rộng câu trả lời bằng một mẫu cho Java 8
Evgeniy Khyst

Trình lặp ba đối số đã được thêm vào trong Java 9
Thorbjørn Ravn Andersen

3

Cá nhân tôi, tôi sẽ rút ngắn lớp DoubleSequenceGenerator lên một chút cho các tính năng khác và chỉ sử dụng một trình tạo trình tự phương thức có chứa tùy chọn để sử dụng bất kỳ độ chính xác mong muốn nào hoặc không sử dụng độ chính xác nào cả:

Trong phương thức trình tạo bên dưới, nếu không có gì (hoặc bất kỳ giá trị nào nhỏ hơn 0) được cung cấp cho tham số setPrecision tùy chọn thì sẽ không thực hiện làm tròn chính xác thập phân. Nếu 0 được cung cấp cho một giá trị chính xác thì các số được làm tròn thành số nguyên gần nhất của chúng (tức là: 89.674 được làm tròn thành 90.0). Nếu một giá trị chính xác cụ thể lớn hơn 0 được cung cấp thì các giá trị được chuyển đổi thành độ chính xác thập phân đó.

BigDecimal được sử dụng ở đây cho ... à .... chính xác:

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

Và trong chính ():

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

Và bảng điều khiển hiển thị:

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

Ý tưởng thú vị, mặc dù tôi thấy một vài vấn đề. 1. Bằng cách thêm valvào mỗi lần lặp, bạn sẽ bị mất độ chính xác phụ gia. Đối với các chuỗi rất lớn, lỗi trên một vài số cuối có thể là đáng kể. 2. Các cuộc gọi lặp đi lặp lại BigDecimal.valueOf()tương đối đắt tiền. Bạn sẽ có được hiệu suất tốt hơn (và độ chính xác) bằng cách chuyển đổi các đầu vào thành BigDecimals và sử dụng BigDecimalcho val. Trên thực tế, bằng cách sử dụng doublefor val, bạn không thực sự nhận được bất kỳ lợi ích chính xác nào từ việc sử dụng BigDecimalngoại trừ việc làm tròn.
NanoWizard

2

Thử cái này.

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

Đây,

int java.math.BigDecimal.scale()

Trả về quy mô của BigDecimal này. Nếu bằng 0 hoặc dương, thang đo là số chữ số ở bên phải dấu thập phân. Nếu âm, giá trị không được tính của số được nhân với mười với sức mạnh của phủ định của thang đo. Ví dụ: thang đo -3 có nghĩa là giá trị không được tính toán được nhân với 1000.

Trong chính ()

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

Và đầu ra:

[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. Có một thư viện hiện có cung cấp loại chức năng này chưa?

    Xin lỗi, tôi không biết, nhưng đánh giá bằng các câu trả lời khác, và sự đơn giản tương đối của chúng - không, không có. Không cần. Chà, gần như ...

  2. Nếu không, có bất kỳ vấn đề với cách tiếp cận của tôi?

    Có và không. Bạn có ít nhất một lỗi và một số chỗ để tăng hiệu suất, nhưng cách tiếp cận là chính xác.

    1. Lỗi của bạn: lỗi làm tròn (chỉ cần thay đổi while (mult*fraction < 1.0)thànhwhile (mult*fraction < 10.0) và sẽ khắc phục nó)
    2. Tất cả những người khác không đạt được end ... tốt, có lẽ họ chỉ không đủ quan sát để đọc các nhận xét trong mã của bạn
    3. Tất cả những người khác chậm hơn.
    4. Chỉ cần thay đổi điều kiện trong vòng lặp chính từ int < Doubleđể int < intđáng chú ý sẽ làm tăng tốc độ của mã của bạn
  3. Có ai có một cách tiếp cận tốt hơn cho điều này?

    Hmm ... theo cách nào?

    1. Sự đơn giản? generateSequenceDoubleStreamcủa @Evgeniy Khyst trông khá đơn giản. Và nên được sử dụng ... nhưng có lẽ không, vì hai điểm tiếp theo
    2. Tóm lược? generateSequenceDoubleStreamkhông phải! Nhưng vẫn có thể được lưu với mô hình start + step*i. Và start + step*imô hình là chính xác. Chỉ BigDoublevà số học điểm cố định có thể đánh bại nó. Nhưng BigDoubles rất chậm và số học điểm cố định thủ công là tẻ nhạt và có thể không phù hợp với dữ liệu của bạn. Nhân tiện, về các vấn đề chính xác, bạn có thể giải trí với điều này: https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
    3. Tốc độ ... bây giờ chúng tôi đang ở trong căn cứ run rẩy. Hãy xem phần này thay thế https://repl.it/repls/RespectfulSu enoughWorker Tôi không có chỗ kiểm tra đàng hoàng ngay bây giờ, vì vậy tôi đã sử dụng repl.it ... điều này hoàn toàn không đủ để kiểm tra hiệu suất, nhưng đó không phải là điểm chính. Vấn đề là - không có câu trả lời chắc chắn. Ngoại trừ có thể trong trường hợp của bạn, điều không hoàn toàn rõ ràng từ câu hỏi của bạn, bạn chắc chắn không nên sử dụng BigDecimal (đọc thêm).

      Tôi đã cố gắng chơi và tối ưu hóa cho đầu vào lớn. Và mã gốc của bạn, với một số thay đổi nhỏ - nhanh nhất. Nhưng có lẽ bạn cần một lượng lớn Lists nhỏ ? Sau đó, đó có thể là một câu chuyện hoàn toàn khác.

      Mã này khá đơn giản theo sở thích của tôi và đủ nhanh:

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

    Nếu bạn thích một cách thanh lịch hơn (hoặc chúng ta nên gọi nó là thành ngữ), cá nhân tôi sẽ đề nghị:

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

    Dù sao, hiệu suất tăng có thể là:

    1. Hãy thử chuyển đổi từ Doublesang double, và nếu bạn thực sự cần chúng, bạn có thể chuyển đổi lại, đánh giá bằng các bài kiểm tra, nó vẫn có thể nhanh hơn. (Nhưng đừng tin tôi, hãy tự thử dữ liệu của bạn trong môi trường của bạn. Như tôi đã nói - repl.it hút điểm chuẩn)
    2. Một phép thuật nhỏ: vòng lặp riêng cho Math.round()... có lẽ nó có liên quan đến địa phương dữ liệu. Tôi không khuyến khích điều này - kết quả rất không ổn định. Nhưng nó rất thú vị.

      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. Bạn chắc chắn nên xem xét để lười biếng hơn và tạo số theo yêu cầu mà không lưu trữ sau đó trong Lists

  4. Tôi nghi ngờ rằng trong hầu hết các trường hợp sử dụng chung, chi phí làm tròn sẽ không đáng kể.

    Nếu bạn nghi ngờ điều gì đó - hãy kiểm tra nó :-) Câu trả lời của tôi là "Có", nhưng một lần nữa ... đừng tin tôi. Kiểm tra nó

Vì vậy, trở lại câu hỏi chính: Có cách nào tốt hơn không?
Phải, tất nhiên!
Nhưng nó phụ thuộc.

  1. Chọn số thập phân lớn nếu bạn cần số rất lớn số rất nhỏ . Nhưng nếu bạn sử dụng chúng trở lại Double, và hơn thế nữa, hãy sử dụng nó với số lượng "gần" - không cần chúng! Kiểm tra cùng một thay thế: https://repl.it/repls/RespectfulSu enoughWorker - thử nghiệm cuối cùng cho thấy rằng sẽ không có sự khác biệt trong kết quả , nhưng mất tốc độ đào.
  2. Thực hiện một số tối ưu hóa vi mô dựa trên các thuộc tính dữ liệu, nhiệm vụ và môi trường của bạn.
  3. Thích mã ngắn và đơn giản nếu không có nhiều để đạt được từ tăng hiệu suất 5-10%. Đừng thắt lưng
  4. Có thể sử dụng số học điểm cố định nếu bạn có thể và nếu nó có giá trị.

Ngoài ra, bạn vẫn ổn.

PS . Ngoài ra còn có một triển khai Công thức tổng hợp Kahan trong thay thế ... chỉ để cho vui. https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html#1346 và nó hoạt động - bạn có thể giảm thiểu lỗi tổng hợp

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.