Di chuyển gấp đôi các vị trí thập phân


97

Vì vậy, tôi có một bộ đôi bằng 1234, tôi muốn di chuyển một chữ số thập phân sang đó thành 12,34

Vì vậy, để làm điều này, tôi nhân .1 với 1234 hai lần, tương tự như thế này

double x = 1234;
for(int i=1;i<=2;i++)
{
  x = x*.1;
}
System.out.println(x);

Thao tác này sẽ in ra kết quả, "12.340000000000002"

Có cách nào, mà không cần định dạng nó thành hai chữ số thập phân, để có lưu trữ kép 12,34 một cách chính xác không?


30
Dưới đây là một liên kết đến bài viết gốc "Tất cả những gì máy tính khoa học nên biết về Floating-Point Arithmetic"
bsd

43
Có lý do gì bạn không làm x /= 100;?
Mark Ingram

Câu trả lời:


189

Nếu bạn sử dụng doublehoặc float, bạn nên sử dụng làm tròn hoặc có thể thấy một số lỗi làm tròn. Nếu bạn không thể làm điều này, hãy sử dụng BigDecimal.

Vấn đề bạn gặp phải là 0,1 không phải là một đại diện chính xác và bằng cách thực hiện phép tính hai lần, bạn đang cộng sai số đó.

Tuy nhiên, 100 có thể được biểu diễn chính xác, vì vậy hãy thử:

double x = 1234;
x /= 100;
System.out.println(x);

mà in:

12.34

Điều này hoạt động vì Double.toString(d)thực hiện một số lượng nhỏ làm tròn thay mặt bạn, nhưng nó không nhiều. Nếu bạn đang tự hỏi nó sẽ trông như thế nào nếu không làm tròn:

System.out.println(new BigDecimal(0.1));
System.out.println(new BigDecimal(x));

bản in:

0.100000000000000005551115123125782702118158340454101562
12.339999999999999857891452847979962825775146484375

Tóm lại, việc làm tròn là không thể tránh khỏi đối với các câu trả lời hợp lý trong dấu phẩy động cho dù bạn có đang làm điều này một cách rõ ràng hay không.


Lưu ý: x / 100x * 0.01không hoàn toàn giống nhau khi nói đến lỗi làm tròn. Điều này là do lỗi làm tròn đối với biểu thức đầu tiên phụ thuộc vào các giá trị của x, trong khi biểu thức 0.01thứ hai có lỗi làm tròn cố định.

for(int i=0;i<200;i++) {
    double d1 = (double) i / 100;
    double d2 = i * 0.01;
    if (d1 != d2)
        System.out.println(d1 + " != "+d2);
}

bản in

0.35 != 0.35000000000000003
0.41 != 0.41000000000000003
0.47 != 0.47000000000000003
0.57 != 0.5700000000000001
0.69 != 0.6900000000000001
0.7 != 0.7000000000000001
0.82 != 0.8200000000000001
0.83 != 0.8300000000000001
0.94 != 0.9400000000000001
0.95 != 0.9500000000000001
1.13 != 1.1300000000000001
1.14 != 1.1400000000000001
1.15 != 1.1500000000000001
1.38 != 1.3800000000000001
1.39 != 1.3900000000000001
1.4 != 1.4000000000000001
1.63 != 1.6300000000000001
1.64 != 1.6400000000000001
1.65 != 1.6500000000000001
1.66 != 1.6600000000000001
1.88 != 1.8800000000000001
1.89 != 1.8900000000000001
1.9 != 1.9000000000000001
1.91 != 1.9100000000000001

26
Tôi không thể tin rằng ngay từ đầu tôi đã không nghĩ đến việc đó! Cảm ơn :-P
BlackCow

6
Mặc dù 100 có thể được biểu diễn chính xác ở định dạng nhị phân, phép chia cho 100 không thể được biểu diễn chính xác. Vì vậy, viết lách 1234/100, như bạn đã làm, không thực sự làm được gì về vấn đề cơ bản - nó phải chính xác như viết 1234 * 0.01.
Brooks Moses

1
@Peter Lawrey: Bạn có thể giải thích thêm tại sao số lẻ hay số chẵn sẽ ảnh hưởng đến việc làm tròn số không? Tôi nghĩ rằng / = 100 và * =. 01 sẽ giống nhau vì mặc dù 100 là số nguyên, nhưng dù sao thì nó cũng sẽ được chuyển đổi thành 100.0 do kiểu ép buộc.
ermzeit

1
/100*0.01tương đương với nhau, nhưng không tương đương với OP *0.1*0.1.
Amadan

1
Tất cả những gì tôi đang nói là nhân với 0,1 hai lần về trung bình sẽ tạo ra sai số lớn hơn nhân với 0,01 một lần; nhưng tôi sẽ vui vẻ thừa nhận quan điểm của @ JasperBekkers về 100 là khác nhau, chính xác là có thể biểu diễn nhị phân.
Amadan

52

Không - nếu bạn muốn lưu trữ chính xác các giá trị thập phân, hãy sử dụng BigDecimal. doublechỉ đơn giản là không thể biểu diễn một số chính xác như 0,1, bạn có thể viết chính xác giá trị của một phần ba với một số hữu hạn các chữ số thập phân.


46

nếu đó chỉ là định dạng, hãy thử printf

double x = 1234;
for(int i=1;i<=2;i++)
{
  x = x*.1;
}
System.out.printf("%.2f",x);

đầu ra

12.34

8
Các câu trả lời được đánh giá cao hơn sẽ sâu sắc hơn về mặt kỹ thuật, nhưng đây là câu trả lời chính xác cho vấn đề của OP. Nói chung, chúng tôi không quan tâm đến độ chính xác nhỏ của double, vì vậy BigDecimal là quá mức cần thiết, nhưng khi hiển thị, chúng tôi thường muốn đảm bảo đầu ra phù hợp với trực giác của mình, do đó, đây System.out.printf()là cách phù hợp.
dimo414

28

Trong phần mềm tài chính, người ta thường sử dụng số nguyên cho đồng xu. Ở trường, chúng tôi được dạy cách sử dụng điểm cố định thay vì thả nổi, nhưng đó thường là lũy thừa của hai. Lưu trữ đồng xu dưới dạng số nguyên cũng có thể được gọi là "điểm cố định".

int i=1234;
printf("%d.%02d\r\n",i/100,i%100);

Trong lớp, chúng tôi được hỏi chung về những con số nào có thể được biểu diễn chính xác trong một cơ số.

Đối với base=p1^n1*p2^n2... bạn có thể biểu diễn N bất kỳ trong đó N = n * p1 ^ m1 * p2 ^ m2.

Hãy để base=14=2^1*7^1... bạn có thể đại diện cho 1/7 1/14 1/28 1/49 nhưng không phải là 1/3

Tôi biết về phần mềm tài chính - Tôi đã chuyển đổi các báo cáo tài chính của Ticketmaster từ VAX asm sang PASCAL. Họ đã có định dạng riêng của họ () với mã cho đồng xu. Lý do chuyển đổi là số nguyên 32 bit không còn đủ nữa. +/- 2 tỷ xu là 20 triệu đô la và con số đó đã tràn vào World Cup hoặc Thế vận hội, tôi quên mất.

Tôi đã thề giữ bí mật. Ồ tốt. Trong academea, nếu nó tốt, bạn xuất bản; trong ngành, bạn giữ bí mật.


12

bạn có thể thử biểu diễn số nguyên

int i =1234;
int q = i /100;
int r = i % 100;

System.out.printf("%d.%02d",q, r);

5
@Dan: Tại sao? Đây là cách tiếp cận chính xác cho các ứng dụng tài chính (hoặc bất kỳ ứng dụng nào khác mà ngay cả một lỗi làm tròn nhỏ cũng không thể chấp nhận được), trong khi vẫn duy trì tốc độ cấp phần cứng. (Tất nhiên, nó sẽ được bao bọc trong một lớp học, bình thường, không viết ra tất cả các thời gian)
Amadan

7
Có một vấn đề nhỏ với giải pháp này - nếu phần còn lại rnhỏ hơn 10, không có khoảng đệm 0 nào xảy ra và 1204 sẽ tạo ra kết quả là 12,4. Chuỗi định dạng đúng tương tự hơn với "% d.% 02d"
jakebman

10

Điều này là do cách máy tính lưu trữ số dấu phẩy động. Họ không làm như vậy chính xác. Là một lập trình viên, bạn nên đọc hướng dẫn về dấu phẩy động này để tự làm quen với những thử nghiệm và khó khăn khi xử lý số dấu phẩy động.


Argh, tôi chỉ đang viết một lời giải thích liên kết đến cùng một nơi. +1.
Pops

@Lord Haha, xin lỗi. Dù sao thì tôi cũng đã bị xóa. :-)
CanSpice

Tôi đã hiểu đó là lý do tại sao, nhưng tôi tự hỏi liệu có một cách sáng tạo nào đó để di chuyển chữ số thập phân qua không? Bởi vì có thể lưu trữ 12,34 một cách rõ ràng trong một nhân đôi, nó chỉ không thích nhân với 0,1
BlackCow

1
Nếu có thể lưu trữ sạch sẽ 12,34 trong hai lần, bạn có nghĩ rằng Java sẽ làm được không? Nó không thể. Bạn sẽ phải sử dụng một số kiểu dữ liệu khác (như BigDecimal). Ngoài ra, tại sao bạn không chia cho 100 thay vì thực hiện nó trong một vòng lặp?
CanSpice

Do'h ... vâng, chia nó cho 100 kết quả là 12,34 sạch ... cảm ơn :-P
BlackCow

9

Thật buồn cười khi nhiều bài đăng đề cập đến việc sử dụng BigDecimal nhưng không ai bận tâm để đưa ra câu trả lời chính xác dựa trên BigDecimal? Bởi vì ngay cả với BigDecimal, bạn vẫn có thể làm sai, như được minh họa bằng mã này

String numstr = "1234";
System.out.println(new BigDecimal(numstr).movePointLeft(2));
System.out.println(new BigDecimal(numstr).multiply(new BigDecimal(0.01)));
System.out.println(new BigDecimal(numstr).multiply(new BigDecimal("0.01")));

Cung cấp đầu ra này

12.34
12.34000000000000025687785232264559454051777720451354980468750
12.34

Hàm tạo BigDecimal đề cập cụ thể rằng tốt hơn nên sử dụng hàm tạo chuỗi hơn là một hàm tạo số. Độ chính xác cuối cùng cũng bị ảnh hưởng bởi MathContext tùy chọn.

Theo BigDecimal Javadoc , có thể tạo BigDecimal chính xác bằng 0,1, miễn là bạn sử dụng hàm tạo chuỗi.


5

Có, có. Với mỗi thao tác kép, bạn có thể mất độ chính xác nhưng mức độ chính xác khác nhau đối với mỗi thao tác và có thể được giảm thiểu bằng cách chọn đúng trình tự thao tác. Ví dụ khi nhân tập hợp số, tốt nhất nên sắp xếp tập hợp theo số mũ trước khi nhân.

Bất kỳ cuốn sách tử tế nào về việc bẻ khóa số đều mô tả điều này. Ví dụ: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Và để trả lời câu hỏi của bạn:

Sử dụng chia thay vì nhân, theo cách này bạn sẽ có kết quả chính xác.

double x = 1234;
for(int i=1;i<=2;i++)
{
  x =  x / 10.0;
}
System.out.println(x);

3

Không, vì các kiểu dấu phẩy động của Java (thực sự là tất cả các kiểu dấu phẩy động) là sự cân bằng giữa kích thước và độ chính xác. Mặc dù chúng rất hữu ích cho nhiều tác vụ, nhưng nếu bạn cần độ chính xác tùy ý, bạn nên sử dụng BigDecimal.

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.