2012-06-28 24 views
9

tôi làm việc chủ yếu với số nguyên trước đây, và trong những tình huống mà tôi cần phải cắt xén một float hay double thành một số nguyên, tôi sẽ sử dụng sau đây trước:Trong C và Objective-C, những gì thực sự là đúng cách để cắt ngắn một phao hoặc đôi để một số nguyên?

(int) someValue 

trừ cho đến khi tôi phát hiện ra những điều sau đây:

NSLog(@"%i", (int) ((1.2 - 1) * 10));  // prints 1 
NSLog(@"%i", (int) ((1.2f - 1) * 10)); // prints 2 

(vui lòng xem Strange behavior when casting a float to int in C# để được giải thích).

Câu hỏi ngắn gọn là: cách chúng tôi nên cắt ngắn một phao hoặc gấp đôi thành số nguyên đúng cách? (Truncation là muốn trong trường hợp này, không phải "làm tròn"). Hoặc, chúng tôi có thể nói rằng vì một số là 1.9999999999999 và số kia là 2.00000000000001 (nói gần), truncate thực sự được thực hiện chính xác. Vì vậy, câu hỏi là, làm thế nào chúng ta nên chuyển đổi một phao hoặc tăng gấp đôi để kết quả là một số "cắt ngắn" mà làm cho cảm giác sử dụng phổ biến?

(mục đích không phải là để sử dụng round, bởi vì trong trường hợp này, cho 1.8, chúng tôi muốn kết quả của 1, thay vì 2) câu hỏi


dài:

tôi đã sử dụng

int truncateToInteger(double a) { 
    return (int) (a + 0.000000000001); 
} 

-(void) someTest { 
    NSLog(@"%i", truncateToInteger((1.2 - 1) * 10)); 
    NSLog(@"%i", truncateToInteger((1.2f - 1) * 10)); 
} 

và cả hai in ra là 2, nhưng có vẻ như quá nhiều hack và những gì số lượng nhỏ chúng ta nên sử dụng để "loại bỏ sự thiếu chính xác"? Có một cách tiêu chuẩn hay được nghiên cứu hơn, thay vì hack tùy ý như vậy?

(Lưu ý rằng chúng tôi muốn cắt bớt, không làm tròn trong một số cách sử dụng, ví dụ: nếu số giây là 90 hoặc 118, khi chúng tôi hiển thị số phút và số giây đã trôi qua, phút sẽ hiển thị dưới dạng 1, nhưng không được làm tròn lên đến 2)

+0

Chức năng 'sàn' có cùng vấn đề không? – Scroog1

+0

có, tầng có cùng vấn đề (đầu tiên sử dụng sàn và sau đó truyền tới int) –

+0

khá là bất tiện: ( – Scroog1

Trả lời

0

Bạn phải tính toán lỗi nào bạn mong đợi và sau đó bạn có thể an toàn thêm lỗi đó cho việc cắt bớt của mình. Ví dụ, bạn nói 1.8 nên được ánh xạ tới 1. Còn khoảng 1.9? Điều gì về 1,99? Nếu bạn biết rằng trong miền có vấn đề của mình, bạn không thể nhận được bất kỳ thứ gì lớn hơn 1.8, bạn có thể an toàn khi thêm 0,001 để thực hiện công việc cắt ngắn.

1

'hack' là cách thích hợp để thực hiện. Nó đơn giản như thế nào nổi hoạt động, nếu bạn muốn hành vi thập phân lành mạnh hơn NSDecimal(Number) có thể là những gì bạn muốn.

+0

Có NSDecimal không? Tôi chỉ có thể tìm thấy NSDecimalNumber https://developer.apple.com/library/mac/# tài liệu/Cocoa/Tham khảo/Nền tảng/Lớp học/NSDecimalNumber_Class/Tham chiếu/Reference.html – nhahtdh

+1

NSDecimal là phiên bản cấu trúc, được ghi lại ở đây: https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation /Miscellaneous/Foundation_DataTypes/Reference/reference.html –

+0

chúng có chức năng sàn hoặc cắt không? –

12

Cắt ngắn đã được thực hiện chính xác, tất nhiên, nhưng trên một giá trị trung gian không chính xác.

Nói chung không có cách nào để biết liệu 1.999999 kết quả của bạn là một chút không chính xác 2 (vì vậy kết quả chính xác toán học sau khi cắt ngắn là 2), hoặc một 1.999998 hơi không chính xác (vì vậy kết quả chính xác toán học sau khi cắt ngắn là 1) .

Đối với vấn đề đó, đối với một số tính toán, bạn có thể nhận được 2.000001 là hơi không chính xác 1.999998. Khá nhiều thứ bạn làm, bạn sẽ hiểu sai. Cắt ngắn là một chức năng không liên tục, do đó, tuy nhiên bạn làm điều đó, nó làm cho tính toán tổng thể của bạn số không ổn định.

Bạn có thể thêm bất kỳ dung sai tùy ý nào: (int)(x > 0 ? x + epsilon : x - epsilon). Nó có thể hoặc tôi không giúp đỡ, tùy thuộc vào những gì bạn đang làm, đó là lý do tại sao nó là một "hack". epsilon có thể là hằng số hoặc có thể chia tỷ lệ theo kích thước của x.

Giải pháp phổ biến nhất cho câu hỏi thứ hai của bạn không phải là "loại bỏ tính không chính xác", thay vì chấp nhận kết quả không chính xác như thể nó là chính xác. Vì vậy, nếu đơn vị dấu chấm động của bạn nói rằng (1.2-1)*10 là 1,999999, OK, nó 1,999999. Nếu giá trị đó đại diện cho một số phút thì nó cắt ngắn thành 1 phút 59 giây. Kết quả hiển thị cuối cùng của bạn sẽ là 1s ngoài giá trị thực. Nếu bạn cần kết quả hiển thị cuối cùng chính xác hơn, thì bạn không nên sử dụng số học dấu phẩy động để tính toán, hoặc có lẽ bạn nên làm tròn đến giây gần nhất trước khi cắt ngắn thành phút.

Bất kỳ cố gắng "xóa" không chính xác khỏi số dấu phẩy thực sự sẽ di chuyển không chính xác xung quanh - một số yếu tố đầu vào sẽ cho kết quả chính xác hơn, số khác kém chính xác hơn. Nếu bạn may mắn đủ để được trong một trường hợp mà sự không chính xác được chuyển sang đầu vào bạn không quan tâm, hoặc có thể lọc ra trước khi thực hiện tính toán, sau đó bạn giành chiến thắng. Nói chung, mặc dù, nếu bạn phải chấp nhận bất kỳ đầu vào sau đó bạn sẽ mất một nơi nào đó. Bạn cần phải xem xét cách làm cho tính toán của bạn chính xác hơn, thay vì cố gắng loại bỏ không chính xác trong một bước cắt ngắn ở cuối.

Có một sửa đổi đơn giản cho phép tính ví dụ của bạn - sử dụng số học điểm cố định với một số thập phân cơ bản-10. Chúng tôi biết rằng định dạng có thể đại diện chính xác 1.2. Vì vậy, thay vì viết (1.2 - 1) * 10, bạn nên rescale tính toán để sử dụng phần mười (viết (12 - 10) * 10) và sau đó chia kết quả cuối cùng bằng 10 để quy mô nó trở lại đơn vị.

+0

Tôi đã +1 câu trả lời của bạn vì câu trả lời là cốt lõi của vấn đề. Nếu OP thực sự muốn xử lý số "rất gần" với số nguyên tiếp theo như là số nguyên tiếp theo, giống như nhân với '(1 + DBL_EPSILON * K)' trước khi cắt xén có thể hoạt động, trong đó 'K' là một hằng số nhỏ gần đúng tỷ lệ với số vòng tròn giá trị ban đầu đã phải chịu. –

+0

'epsilon' nên là gì? Tôi nghĩ rằng việc sử dụng '0.0000001' có thể là ok, nhưng lần sau trong một chương trình khác, nếu tôi sử dụng '0.00000000000000000000000001' hoặc một số thậm chí nhỏ hơn, nó có thể không đủ để" khắc phục sự thiếu chính xác "... xử lý nó? –

+0

@ 動靜 能量: Giống như R .. nói, nó phụ thuộc vào tính toán bạn đã làm. –

0

Cách đúng để thực hiện điều đó là: Xác định từng thao tác dấu phẩy động mà bạn thực hiện. Điều này bao gồm chuyển đổi chữ số thập phân sang dấu phẩy động (chẳng hạn như “1.2” trong văn bản nguồn tạo ra giá trị dấu phẩy động 0x1.3333333333333p0 hoặc “1.2f” tạo 0x1.333334p0). Xác định giới hạn về lỗi mà mỗi thao tác có thể tạo ra. (Đối với các hoạt động cơ bản được IEEE 754 xác định, chẳng hạn như số học đơn giản, giới hạn này là 1/2 ULP [đơn vị độ chính xác ít nhất] của kết quả chính xác về mặt toán học của đầu vào thực tế. đặc điểm kỹ thuật có thể cho phép 1 ULP, nhưng trình biên dịch tốt sẽ giới hạn nó để 1/2 ULP.Đối với thói quen thư viện cung cấp các chức năng phức tạp như sin hoặc logarit, thư viện thương mại thường cho phép một số ULP lỗi, mặc dù chúng thường tốt hơn trong khoảng thời gian cơ sở. để có được một đặc điểm kỹ thuật từ nhà cung cấp thư viện.) Xác định giới hạn về lỗi cuối cùng, với một bằng chứng toán học. Nếu bạn có thể chứng minh rằng, đối với một số lỗi bị ràng buộc e, khi kết quả toán học chính xác là một số nguyên i, kết quả tính toán thực tế là trong khoảng thời gian nửa mở [tức là, i + 1-e), thì bạn có thể tạo ra kết quả toán học chính xác bằng cách thêm e vào kết quả tính toán và cắt ngắn kết quả của phép tính đó thành một số nguyên. Một là vấn đề thêm e có thể gây ra làm tròn lên đến i + 1. Một là tránh dương tính giả, có nghĩa là, tránh sản xuất i khi kết quả không phải là i, có thể là do lỗi cuối cùng khi kết quả thực tế không phải là tôi có thể đưa kết quả tính toán vào [tức là, i + 1-e).)

Như bạn có thể thấy, cách “đúng” nói chung rất khó. Đối với mã phức tạp, các bằng chứng chỉ được tạo ra trong các trường hợp có giá trị cao hạn chế, chẳng hạn như thiết kế các thường trình thư viện chất lượng cao để tính các hàm thư viện toán chuẩn (sin, logarit, et cetera).

Đối với mã đơn giản, bằng chứng có thể đơn giản.Nếu bạn biết câu trả lời nên được chính xác một số nguyên, và bạn biết bạn đã không làm như vậy nhiều hoạt động nổi-điểm rằng lỗi không thể đã trở thành lớn như 0,5, sau đó một cách đúng đắn để tạo ra đúng câu trả lời chỉ đơn giản là thêm .5 và cắt ngắn. Không có gì sai với điều này, bởi vì nó được chứng minh là đúng. (Trên thực tế, nó không chỉ là số lượng các hoạt động mà bạn thực hiện mà là bản chất của chúng. Phép trừ các giá trị với độ lớn tương tự là khét tiếng để tạo ra các lỗi mà lỗi tương đối là rất lớn. lỗi.)

Nếu bạn không biết câu trả lời đúng về mặt toán học chính xác là một số nguyên thì việc cắt xén là sai. Nếu bạn không biết một ràng buộc về lỗi tính toán của bạn, sau đó thêm bất kỳ sửa chữa trước khi cắt xén là sai. Không có câu trả lời chung cho vấn đề này; bạn phải hiểu tính toán của mình.

+0

cảm ơn, nhưng xin lưu ý nó không phải là "tròn" ... nó hoàn toàn cắt ngắn –

+0

thực sự, hầu hết các tính toán tôi làm, số đại diện trong phao hoặc đôi thường nhỏ hơn 1000 hoặc 2000 (tọa độ), và gần như luôn nhỏ hơn giá trị số nguyên 32 bit ... –

+0

Loại "cắt ngắn" bạn đang yêu cầu ** không cắt ngắn **. Đó là một loại làm tròn. Và nó không tồn tại. –

3

Khi bạn đã sửa đổi câu hỏi của mình, vấn đề bây giờ có vẻ như thế này: Với một số đầu vào x, bạn tính giá trị f '(x). f '(x) là xấp xỉ được tính toán cho hàm toán học chính xác f (x). Bạn muốn tính toán trunc (f (x)), tức là, số nguyên i nằm xa nhất từ ​​0 mà không xa hơn f (x). Bởi vì f '(x) có một số lỗi, trunc (f' (x)) có thể không bằng trunc (f (x)), chẳng hạn như khi f (x) là 2 nhưng f '(x) là 0x1.fffffffffffffp0. Cho f '(x), làm thế nào bạn có thể tính toán trunc (f (x))?

Vấn đề này không thể giải quyết được. Có không có giải pháp sẽ hoạt động cho tất cả f.

Lý do không có giải pháp là do lỗi trong f ', f' (x) có thể là 0x1.fffffffffffffp0 vì f (x) là 0x1.fffffffffffffp0 hoặc f '(x) có thể là 0x1 .fffffffffffffp0 vì các lỗi tính toán mặc dù f (x) là 2. Do đó, với một giá trị cụ thể của f '(x), không thể biết trunc (f (x)) là gì.

Một giải pháp là có thể chỉ cung cấp thông tin chi tiết về f (và các hoạt động thực tế được sử dụng để ước tính nó với f '). Bạn đã không đưa ra thông tin đó, vì vậy câu hỏi của bạn không thể được trả lời. Đây là giả thuyết: Giả sử bản chất của f (x) là kết quả của nó luôn luôn là một bội số âm của q, đối với một số q chia cho 1. Ví dụ, q có thể là 0,01 (hàng trăm phần trăm của giá trị tọa độ) hoặc 1/60 (đại diện cho đơn vị giây vì f là đơn vị phút). Và giả sử các giá trị và hoạt động được sử dụng để tính toán f 'là sao cho lỗi trong f' luôn nhỏ hơn q/2.

Trong trường hợp rất hạn chế này, giả sử, sau đó cắt ngắn (f (x)) có thể được tính bằng cách tính toán trunc (f '(x) + q/2). Bằng chứng: Hãy để tôi = trunc (f (x)). Giả sử i> 0. Sau đó, i < = f (x) < i + 1, vì vậy tôi < = f (x) < = i + 1-q (vì f (x) được lượng tử hóa bởi q). Sau đó i-q/2 < f '(x) < i + 1-q + q/2 (vì f' (x) nằm trong q/2 của f (x)). Sau đó, i < f '(x) + q/2 < i + 1. Sau đó, trunc (f '(x) + q/2) = i, vì vậy chúng tôi có kết quả mong muốn. Trong trường hợp i = 0, thì -1 < f (x) < 1, vì vậy -1 + q < = f (x) < = 1-q, vì vậy -1 + qq/2 < f '(x) < 1-q + q/2, vì vậy -1 + q < f '(x) + q/2 < 1, vì vậy trunc (f' (x) + q/2) = 0.

(Lưu ý: Nếu q/2 không chính xác thể hiện trong độ chính xác của dấu phẩy động được sử dụng hoặc không thể được thêm vào f '(x) không có lỗi, sau đó một số điều chỉnh phải được thực hiện trong bằng chứng, điều kiện của nó hoặc bổ sung q/2 .)

Nếu trường hợp đó không phục vụ cho mục đích của bạn thì bạn không thể mong đợi câu trả lời bằng cách cung cấp thông tin chi tiết về f và các hoạt động và giá trị được sử dụng để tính toán f '.

1

Tôi khuyên bạn nên không bao giờ mong đợi kết quả của bạn có độ chính xác cao hơn đầu vào của bạn. Vì vậy, trong ví dụ của bạn, phao của bạn có một chữ số thập phân và bạn không cần phải đưa kết quả của bạn trở nên nghiêm trọng hơn thế.

Vậy làm thế nào để làm tròn đến một chữ số thập phân, sau đó chuyển thành int?

float a = (1.2f - 1) * 10; 
int b; 

// multiply by 10 to "round to one decimal place" 
a = round(a * 10.); 

// now cast to integer first to avoid further decimal errors 
b = (int) a; 

// get rid of the factor 10 again by integer division 
b = b/10; 

// now 'b' should hold the result you're expecting; 
+0

vòng tại một điểm thập phân nhất định và sau đó cắt ngắn, đó là một suy nghĩ thú vị ... (và hy vọng, sẽ không có trường hợp số được làm tròn thành 2.0 hoặc 1384729487.0, nhưng cắt ngắn thành một thứ không mong muốn) –

+0

Bằng cách làm tròn độ chính xác ban đầu của bạn, bạn sẽ loại bỏ các lỗi ở các chữ số thập phân nhỏ, bất kể chúng là gì. Khi bạn nhân với 10 trong câu lệnh ban đầu của bạn, bạn nên luôn luôn có bội số của 10 sau khi làm tròn, do đó, số nguyên chia cho 10 là an toàn theo bất kỳ cách nào. Nhưng ngay cả khi kết quả mong đợi của bạn có số thập phân và nên là 1,9 thay vì 2 bạn sẽ nhận được b = 19/10 mà sẽ là một trong những bạn muốn nó. Tôi không thấy bất kỳ vấn đề ẩn nấp trong bóng tối ở đây. – inVader

1
NSLog(@"%i", [[NSNumber numberWithFloat:((1.2 - 1) * 10)] intValue]); //2 
NSLog(@"%i", [[NSNumber numberWithFloat:(((1.2f - 1) * 10))] intValue]); //2 
NSLog(@"%i", [[NSNumber numberWithFloat:1.8] intValue]); //1 
NSLog(@"%i", [[NSNumber numberWithFloat:1.8f] intValue]); //1 
NSLog(@"%i", [[NSNumber numberWithDouble:2.0000000000001 ] intValue]);//2 
+0

thú vị ... bạn có biết nó hoạt động như thế nào không? –

+0

Nhưng nó sẽ phá vỡ khi nó là 'NSLog (@"% i ", [[Số NSNumberWithDouble: ((1,2 - 1) * 10)] intValue]);' in '1' –

+0

Hãy xem http: //www.koders.com/objectivec/fid92844506886795F3D6CA4417B418B12A1F7EB99B.aspx –

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