2010-07-28 46 views
19

thể trùng lặp:
problem in comparing double values in C#Tại sao vòng lặp này không bao giờ kết thúc?

Tôi đã đọc nó ở nơi khác, nhưng thực sự quên câu trả lời vì vậy tôi yêu cầu ở đây một lần nữa. Vòng lặp này dường như không bao giờ chấm dứt bất kể bạn mã nó trong bất kỳ ngôn ngữ (tôi thử nghiệm nó trong C#, C++, Java ...):

double d = 2.0; 
while(d != 0.0){ 
    d = d - 0.2; 
} 
+2

Không bao giờ sử dụng '==' với giá trị thả nổi. Có thể sử dụng một cái gì đó như 'f> epsilon'. – pascal

+20

[Đã một vài ngày, tôi cho rằng chúng tôi đã hết hạn.] (Http://docs.sun.com/source/806-3568/ncg_goldberg.html) – GManNickG

+0

Trong Java, tôi nghĩ rằng có công cụ sửa đổi "Strictfp" cho những tình huống như vậy –

Trả lời

32

Floating tính toán điểm không hoàn hảo chính xác. Bạn sẽ nhận được một lỗi biểu diễn vì 0,2 không có biểu diễn chính xác dưới dạng số dấu phẩy động nhị phân để giá trị không trở thành chính xác bằng không. Hãy thử thêm một tuyên bố debug để xem vấn đề:

double d = 2.0; 
while (d != 0.0) 
{ 
    Console.WriteLine(d); 
    d = d - 0.2; 
} 
 
2 
1,8 
1,6 
1,4 
1,2 
1 
0,8 
0,6 
0,4 
0,2 
2,77555756156289E-16 // Not exactly zero!! 
-0,2 
-0,4 

Một cách để giải quyết nó là sử dụng các loại decimal.

+1

Và tại sao có lỗi tròn? Loại 'đôi' lưu trữ các chữ số nhị phân. Bởi vì 0,2 bằng văn bản nhị phân là phân số định kỳ, chúng ta phải cắt bỏ một cái gì đó để viết số trên N bit. – adf88

27

(Đối với một điều bạn không sử dụng cùng một biến trong suốt, nhưng tôi sẽ giả định đó là lỗi đánh máy :)

0.2 không thực sự 0,2. Đó là giá trị double gần nhất với 0,2. Khi bạn đã trừ 10 lần từ 2,0, bạn sẽ không kết thúc với chính xác 0.0.

Trong C#, bạn có thể chuyển sang sử dụng các loại decimal thay vào đó, mà sẽ làm việc:

// Works 
decimal d = 2.0m; 
while (d != 0.0m) { 
    d = d - 0.2m; 
} 

này hoạt động vì loại thập phân không đại diện cho giá trị thập phân như 0,2 chính xác (trong giới hạn; đó là một 128- loại bit). Mọi giá trị có liên quan đều thể hiện chính xác, vì vậy nó hoạt động. Có gì sẽ không công việc sẽ là:

decimal d = 2.0m; 
while (d != 0.0m) { 
    d = d - 1m/3m; 
} 

Ở đây, "một phần ba" là không chính xác vì vậy chúng tôi biểu diễn kết thúc với cùng một vấn đề như trước đây.

Nói chung, bạn nên thực hiện so sánh bình đẳng chính xác giữa các số dấu phẩy động - thường là bạn so sánh chúng trong một dung sai nhất định.

Tôi có các bài viết trên floating binary pointfloating decimal point từ ngữ cảnh C# /. NET, giải thích mọi thứ chi tiết hơn.

1

f là uninitialised;)

Nếu bạn muốn tìm:

double f = 2.0; 

Đây có thể là một tác động của arthimetic không chính xác trên các biến kép.

3

Bạn đang tốt hơn bằng cách sử dụng

while(f > 0.0) 

* chỉnh sửa: Xem comment pascal của bên dưới. Nhưng nếu bạn cần chạy một vòng lặp, một số lần xác định, thay vì sử dụng một kiểu dữ liệu không thể tách rời.

+0

Tôi biết tôi nên sử dụng nó, nhưng tại sao f! = 0.0 không hoạt động? –

+1

Bạn có thể chạy một lần quá nhiều, nếu 'f' cuối cùng là 2.0e-16 ... – pascal

1

đó là do độ chính xác của dấu phẩy động. sử dụng trong khi (d> 0,0) hoặc nếu bạn phải,

while (Math.abs(d-0.0) > some_small_value){ 

} 
2

Vấn đề là số học điểm động. Nếu không có đại diện nhị phân chính xác cho số, thì bạn chỉ có thể lưu số gần nhất với số đó (giống như bạn không thể lưu số 1/3 bằng số thập phân - bạn chỉ có thể lưu trữ một số thứ như 0.33333333 cho một số độ dài '3's.) Điều này có nghĩa là số học trên các số dấu phẩy động khá thường không hoàn toàn chính xác. Hãy thử một cái gì đó như sau (Java):

public class Looping { 

    public static void main(String[] args) { 

     double d = 2.0; 
     while(d != 0.0 && d >= 0.0) { 
      System.out.println(d); 
      d = d - 0.2; 
     } 

    } 

} 

đầu ra của bạn nên được một cái gì đó như:

2.0 
1.8 
1.6 
1.4000000000000001 
1.2000000000000002 
1.0000000000000002 
0.8000000000000003 
0.6000000000000003 
0.4000000000000003 
0.2000000000000003 
2.7755575615628914E-16 

Và bây giờ bạn sẽ có thể thấy lý do tại sao điều kiện d == 0 bao giờ xảy ra. . (Số cuối cùng có một con số đó là rất gần 0 nhưng không hoàn toàn

Đối với một ví dụ về điểm weirdness nổi, hãy thử này:

public class Squaring{ 

    public static void main(String[] args) { 

     double d = 0.1; 
     System.out.println(d*d); 

    } 

} 

Vì không có biểu diễn nhị phân của chính xác 0.1, bình phương nó không tạo ra kết quả bạn mong đợi (0.01), nhưng thực sự giống như 0.010000000000000002!

+1

" Không bao giờ hoàn toàn chính xác "đang phóng đại nó, IMO. Chỉ vì không * mọi * giá trị thập phân không chính xác thể hiện trong điểm nổi nhị phân không làm cho việc bổ sung chính xác được biểu diễn 0,25 và 0,25 để có được chính xác 0,5 bất kỳ chính xác ít hơn ví dụ. –

+0

Đã chỉnh sửa, cảm ơn Jon - nó hơi bị phóng đại. – Stephen

10

Tôi nhớ mua một Sinclair ZX-81, làm việc theo cách của tôi thông qua hướng dẫn lập trình cơ bản tuyệt vời và gần như trở về đến cửa hàng khi tôi bắt gặp lỗi làm tròn điểm nổi đầu tiên của tôi.

Tôi chưa từng tưởng tượng rằng mọi người vẫn gặp phải những vấn đề này sau 27,99998 năm sau đó.

+2

Hướng dẫn sử dụng ZX-Spectrum đi kèm với giải thích chi tiết về vấn đề này. Tôi có thể nhớ suy nghĩ kỹ về nó (vào khoảng 10 hoặc 11 tuổi) và sẽ “ồ, điều đó có ý nghĩa”. Đó là một hướng dẫn rất tốt ... –

+7

+1 cho 27.99998 năm :-) –

0

Nó không chỉ dừng lại vì 0,2 i không được đại diện một cách chính xác trong hai của bổ sung nên vòng lặp của bạn không bao giờ thực hiện các thử nghiệm 0.0==0.0

+0

Bổ sung của hai không có bất kỳ kết nối nào với lỗi làm tròn số dấu phẩy động. – Egon

0

Như những người khác đã nói, đây chỉ là một vấn đề cơ bản mà bạn nhận được khi làm dấu chấm động số học đến bất kỳ cơ sở nào. Nó chỉ xảy ra rằng base-2 là một trong những phổ biến nhất trong máy tính (vì nó thừa nhận thực hiện phần cứng hiệu quả).

Khắc phục tốt nhất, nếu có thể, là chuyển sang sử dụng một số loại biểu diễn thương của số cho vòng lặp của bạn, làm cho giá trị dấu phẩy động được bắt nguồn từ đó. OK, điều đó nghe quá mức! Đối với trường hợp cụ thể của bạn, tôi sẽ viết như sau:

int dTimes10 = 20; 
double d; 
while(dTimes10 != 0) { 
    dTimes10 -= 2; 
    d = dTimes10/10.0; 
} 

Ở đây, chúng tôi thực sự làm việc với phân số [20/10, 18/10, 16/10, ..., 2/10, 0/10] trong đó phép lặp được thực hiện với các số nguyên (ví dụ, dễ dàng để có được chính xác) trong tử số với một mẫu số cố định, trước khi chuyển đổi sang dấu phẩy động. Nếu bạn có thể viết lại thực để làm việc như thế này, bạn sẽ có thành công lớn (và chúng thực sự không đắt hơn nhiều so với những gì bạn đang làm trước đó, đó là một sự cân bằng tuyệt vời để có được tính chính xác).

Nếu bạn không thể thực hiện việc này, bạn cần sử dụng so sánh trong-epsilon như so sánh của bạn. Khoảng, thay thế d != target bằng abs(d - target) < ε, trong đó lựa chọn ε (epsilon) đôi khi có thể khó xử. Về cơ bản, giá trị đúng của ε phụ thuộc vào một loạt các yếu tố, nhưng nó có thể được chọn tốt nhất là 0.001 cho phép lặp lại ví dụ cho thang tỷ lệ của giá trị bước (nghĩa là, nó bằng nửa phần trăm của độ lớn của bước, vì vậy bất kỳ thứ gì bên trong đó sẽ là lỗi thay vì thông tin).

Các vấn đề liên quan