Làm thế nào tôi có thể đảm bảo rằng một bộ phận số nguyên luôn được làm tròn?


242

Tôi muốn đảm bảo rằng một bộ phận số nguyên luôn được làm tròn nếu cần thiết. Có cách nào tốt hơn thế này không? Có rất nhiều diễn viên đang diễn ra. :-)

(int)Math.Ceiling((double)myInt1 / myInt2)

48
Bạn có thể xác định rõ hơn những gì bạn cho là "tốt hơn" không? Nhanh hơn? Ngắn hơn? Chính xác hơn? Mạnh mẽ hơn? Rõ ràng là đúng?
Eric Lippert

6
Bạn luôn có rất nhiều vai diễn với toán học trong C # - đó là lý do tại sao nó không phải là một ngôn ngữ tuyệt vời cho loại điều này. Bạn có muốn các giá trị được làm tròn lên hoặc cách xa 0 - nên -3.1 chuyển sang -3 (tăng) hoặc -4 (cách xa 0)
Keith

9
Eric: Ý của bạn là gì "Chính xác hơn? Mạnh mẽ hơn? Rõ ràng là chính xác hơn?" Trên thực tế những gì tôi đã làm chỉ là "tốt hơn", tôi sẽ để người đọc đưa ý nghĩa vào tốt hơn. Vì vậy, nếu ai đó có một đoạn mã ngắn hơn, thật tuyệt, nếu một người khác có tốc độ nhanh hơn, cũng tuyệt vời :-) Bạn có đề xuất gì về bạn?
Karsten

1
Tôi có phải là người duy nhất, khi đọc tiêu đề, "Ồ, đó là một loại hình tròn của C #?"
Matt Ball

6
Thật đáng kinh ngạc khi câu hỏi này trở nên khó khăn đến thế nào, và cuộc thảo luận đã mang tính hướng dẫn như thế nào.
Justin Morgan

Câu trả lời:


669

CẬP NHẬT: Câu hỏi này là chủ đề của blog của tôi vào tháng 1 năm 2013 . Cảm ơn vì câu hỏi tuyệt vời của bạn!


Lấy số nguyên đúng là khó. Như đã được chứng minh amply cho đến nay, thời điểm bạn cố gắng thực hiện một mẹo "thông minh", tỷ lệ cược là tốt mà bạn đã phạm sai lầm. Và khi một lỗ hổng được tìm thấy, thay đổi mã để sửa lỗi mà không xem xét liệu sửa lỗi có làm hỏng thứ gì khác không phải là một kỹ thuật giải quyết vấn đề tốt. Cho đến nay chúng tôi đã nghĩ rằng năm giải pháp số học số nguyên không chính xác khác nhau cho vấn đề hoàn toàn không đặc biệt khó khăn này được đăng.

Cách đúng đắn để tiếp cận các vấn đề số học số nguyên - đó là cách làm tăng khả năng nhận được câu trả lời ngay lần đầu tiên - là tiếp cận vấn đề một cách cẩn thận, giải quyết từng bước một và sử dụng các nguyên tắc kỹ thuật tốt để thực hiện vì thế.

Bắt đầu bằng cách đọc thông số kỹ thuật cho những gì bạn đang cố gắng thay thế. Các đặc điểm kỹ thuật cho phân chia số nguyên nêu rõ:

  1. Bộ phận làm tròn kết quả về không

  2. Kết quả bằng 0 hoặc dương khi hai toán hạng có cùng dấu và 0 hoặc âm khi hai toán hạng có dấu trái ngược nhau

  3. Nếu toán hạng bên trái là int đại diện nhỏ nhất và toán hạng bên phải là1, thì xảy ra tràn. [...] Nó được định nghĩa theo triển khai là liệu [ArithaturesException] có bị ném hay tràn không được báo cáo với giá trị kết quả là giá trị của toán hạng bên trái.

  4. Nếu giá trị của toán hạng bên phải bằng 0, System.DivideByZeroException sẽ bị ném.

Những gì chúng ta muốn là một hàm chia số nguyên tính toán thương số nhưng làm tròn kết quả luôn luôn hướng lên trên , không phải luôn luôn hướng về không .

Vì vậy, viết một đặc điểm kỹ thuật cho chức năng đó. Chức năng của chúng tôi int DivRoundUp(int dividend, int divisor)phải có hành vi được xác định cho mọi đầu vào có thể. Hành vi không xác định đó là rất đáng lo ngại, vì vậy hãy loại bỏ nó. Chúng tôi sẽ nói rằng hoạt động của chúng tôi có đặc điểm kỹ thuật này:

  1. hoạt động ném nếu ước số bằng không

  2. hoạt động ném nếu cổ tức là int.minval và ước số là -1

  3. nếu không có phần còn lại - phép chia là 'chẵn' - thì giá trị trả về là thương số nguyên

  4. Mặt khác, nó trả về số nguyên nhỏ nhất lớn hơn thương số, nghĩa là nó luôn làm tròn lên.

Bây giờ chúng tôi có một đặc điểm kỹ thuật, vì vậy chúng tôi biết rằng chúng tôi có thể đưa ra một thiết kế thử nghiệm . Giả sử chúng ta thêm một tiêu chí thiết kế bổ sung rằng vấn đề chỉ được giải quyết bằng số học số nguyên, thay vì tính thương số là gấp đôi, vì giải pháp "nhân đôi" đã bị từ chối rõ ràng trong tuyên bố vấn đề.

Vậy chúng ta phải tính toán cái gì? Rõ ràng, để đáp ứng thông số kỹ thuật của chúng tôi trong khi chỉ còn lại trong số học số nguyên, chúng tôi cần biết ba sự thật. Đầu tiên, số nguyên là gì? Thứ hai, là sự phân chia miễn phí? Và thứ ba, nếu không, là số nguyên được tính bằng cách làm tròn lên hoặc xuống?

Bây giờ chúng ta có một đặc tả và thiết kế, chúng ta có thể bắt đầu viết mã.

public static int DivRoundUp(int dividend, int divisor)
{
  if (divisor == 0 ) throw ...
  if (divisor == -1 && dividend == Int32.MinValue) throw ...
  int roundedTowardsZeroQuotient = dividend / divisor;
  bool dividedEvenly = (dividend % divisor) == 0;
  if (dividedEvenly) 
    return roundedTowardsZeroQuotient;

  // At this point we know that divisor was not zero 
  // (because we would have thrown) and we know that 
  // dividend was not zero (because there would have been no remainder)
  // Therefore both are non-zero.  Either they are of the same sign, 
  // or opposite signs. If they're of opposite sign then we rounded 
  // UP towards zero so we're done. If they're of the same sign then 
  // we rounded DOWN towards zero, so we need to add one.

  bool wasRoundedDown = ((divisor > 0) == (dividend > 0));
  if (wasRoundedDown) 
    return roundedTowardsZeroQuotient + 1;
  else
    return roundedTowardsZeroQuotient;
}

Điều này có thông minh không? Không đẹp? Số ngắn? Không đúng theo đặc điểm kỹ thuật? Tôi tin là như vậy, nhưng tôi chưa kiểm tra đầy đủ. Có vẻ khá tốt mặc dù.

Chúng tôi là những chuyên gia ở đây; sử dụng thực hành kỹ thuật tốt. Nghiên cứu các công cụ của bạn, xác định hành vi mong muốn, xem xét các trường hợp lỗi trước và viết mã để nhấn mạnh tính chính xác rõ ràng của nó. Và khi bạn tìm thấy một lỗi, hãy xem xét liệu thuật toán của bạn có thiếu sót sâu sắc để bắt đầu hay không trước khi bạn ngẫu nhiên bắt đầu hoán đổi các hướng so sánh xung quanh và phá vỡ những thứ đã hoạt động.


44
Câu trả lời mẫu mực tuyệt vời
Gavin Miller

61
Điều tôi quan tâm không phải là hành vi; hoặc là hành vi có vẻ hợp lý. Điều tôi quan tâm là nó không được chỉ định , điều đó có nghĩa là nó không thể dễ dàng được kiểm tra. Trong trường hợp này, chúng tôi đang xác định toán tử riêng của mình, vì vậy chúng tôi có thể chỉ định bất kỳ hành vi nào chúng tôi muốn. Tôi không quan tâm hành vi đó là "ném" hay "không ném", nhưng tôi quan tâm rằng nó được nêu rõ.
Eric Lippert

68
Chết tiệt, nhà sư phạm thất bại :(
Jon Skeet

32
Man - Bạn có thể viết một cuốn sách về điều đó không?
xtofl

76
@finnw: Dù tôi đã thử hay chưa thì không liên quan. Giải bài toán số nguyên này không phải là vấn đề kinh doanh của tôi ; nếu có, thì tôi sẽ thử nó. Nếu ai đó muốn lấy mã từ những người lạ ra khỏi internet để giải quyết vấn đề kinh doanh của họ thì trách nhiệm của họ là kiểm tra kỹ lưỡng.
Eric Lippert

49

Tất cả các câu trả lời ở đây cho đến nay có vẻ khá phức tạp.

Trong C # và Java, đối với cổ tức và ước số dương, bạn chỉ cần thực hiện:

( dividend + divisor - 1 ) / divisor 

Nguồn: Chuyển đổi số, Roland Backhouse, 2001


Tuyệt vời. Mặc dù, bạn nên thêm dấu ngoặc đơn, để loại bỏ sự mơ hồ. Bằng chứng cho nó hơi dài, nhưng bạn có thể cảm thấy nó trong ruột, rằng nó đúng, chỉ bằng cách nhìn vào nó.
Jörgen Sigvardsson

1
Hmmm ... còn cổ tức = 4, số chia = (- 2) thì sao? 4 / (-2) = (-2) = (-2) sau khi được làm tròn lên. nhưng thuật toán bạn đã cung cấp (4 + (-2) - 1) / (-2) = 1 / (-2) = (-0.5) = 0 sau khi được làm tròn lên.
Scott

1
@Scott - xin lỗi, tôi đã bỏ qua đề cập rằng giải pháp này chỉ giữ cho cổ tức và ước số dương. Tôi đã cập nhật câu trả lời của tôi để đề cập làm rõ điều này.
Ian Nelson

1
tôi thích nó, tất nhiên bạn có thể có một phần tràn nhân tạo trong tử số như một sản phẩm phụ của phương pháp này ...
TCC

2
@PIntag: Ý tưởng là tốt, nhưng việc sử dụng modulo sai. Lấy 13 và 3. Dự kiến ​​kết quả 5, nhưng ((13-1)%3)+1)cho 1 kết quả. Lấy đúng loại phân chia, 1+(dividend - 1)/divisorcho kết quả giống như câu trả lời cho cổ tức và ước số dương. Ngoài ra, không có vấn đề tràn, tuy nhiên chúng có thể là nhân tạo.
Lutz Lehmann

48

Câu trả lời dựa trên int cuối cùng

Đối với số nguyên đã ký:

int div = a / b;
if (((a ^ b) >= 0) && (a % b != 0))
    div++;

Đối với số nguyên không dấu:

int div = a / b;
if (a % b != 0)
    div++;

Lý do cho câu trả lời này

Phân chia số nguyên ' /' được xác định để làm tròn về 0 (7.7.2 của thông số kỹ thuật), nhưng chúng tôi muốn làm tròn số. Điều này có nghĩa là các câu trả lời phủ định đã được làm tròn một cách chính xác, nhưng các câu trả lời tích cực cần được điều chỉnh.

Câu trả lời tích cực khác không rất dễ phát hiện, nhưng câu trả lời bằng 0 khó hơn một chút, vì đó có thể là làm tròn giá trị âm hoặc làm tròn số dương.

Đặt cược an toàn nhất là phát hiện khi nào câu trả lời phải dương tính bằng cách kiểm tra xem các dấu hiệu của cả hai số nguyên có giống nhau không. Toán tử xor ' ^' trên hai giá trị sẽ dẫn đến bit 0 khi trường hợp này có nghĩa là kết quả không âm, do đó kiểm tra (a ^ b) >= 0xác định rằng kết quả phải có giá trị dương trước khi làm tròn. Cũng lưu ý rằng đối với các số nguyên không dấu, mọi câu trả lời rõ ràng là tích cực, vì vậy kiểm tra này có thể được bỏ qua.

Việc kiểm tra duy nhất còn lại là liệu có bất kỳ làm tròn nào đã xảy ra hay không, a % b != 0sẽ thực hiện công việc.

Bài học kinh nghiệm

Số học (số nguyên hoặc cách khác) gần như không đơn giản. Suy nghĩ cẩn thận yêu cầu mọi lúc.

Ngoài ra, mặc dù câu trả lời cuối cùng của tôi có lẽ không phải là "đơn giản" hay "rõ ràng" hay thậm chí là "nhanh" như câu trả lời dấu phẩy động, nó có một chất lượng hoàn lại rất mạnh đối với tôi; Bây giờ tôi đã suy luận thông qua câu trả lời, vì vậy tôi thực sự chắc chắn rằng nó là chính xác (cho đến khi ai đó thông minh hơn nói với tôi - ánh mắt giận dữ theo hướng của Eric -).

Để có cùng cảm giác chắc chắn về câu trả lời dấu phẩy động, tôi phải suy nghĩ nhiều hơn (và có thể phức tạp hơn) về việc liệu có bất kỳ điều kiện nào mà độ chính xác của dấu phẩy động có thể cản trở hay Math.Ceilingkhông , và có lẽ một cái gì đó không mong muốn trên đầu vào 'chỉ bên phải'.

Con đường đã đi

Thay thế (lưu ý tôi đã thay thế thứ hai myInt1bằng myInt2, giả sử đó là những gì bạn muốn nói):

(int)Math.Ceiling((double)myInt1 / myInt2)

với:

(myInt1 - 1 + myInt2) / myInt2

Nhắc nhở duy nhất là nếu myInt1 - 1 + myInt2vượt quá loại số nguyên bạn đang sử dụng, bạn có thể không nhận được những gì bạn mong đợi.

Lý do điều này là sai : -1000000 và 3999 nên cho -250, điều này mang lại -249

EDIT:
Xem xét điều này có cùng lỗi với giải pháp số nguyên khác cho các myInt1giá trị âm , có thể dễ dàng hơn để làm một cái gì đó như:

int rem;
int div = Math.DivRem(myInt1, myInt2, out rem);
if (rem > 0)
  div++;

Điều đó sẽ cho kết quả chính xác khi divchỉ sử dụng các phép toán số nguyên.

Lý do điều này là sai : -1 và -5 nên cho 1, điều này cho 0

EDIT (một lần nữa, với cảm giác):
Toán tử chia làm tròn về 0; Đối với kết quả âm tính, điều này là hoàn toàn chính xác, vì vậy chỉ những kết quả không âm mới cần điều chỉnh. Ngoài ra, xem xét rằng DivRemchỉ cần thực hiện một /và một %, hãy bỏ qua cuộc gọi (và bắt đầu với việc so sánh dễ dàng để tránh tính toán modulo khi không cần thiết):

int div = myInt1 / myInt2;
if ((div >= 0) && (myInt1 % myInt2 != 0))
    div++;

Lý do điều này là sai : -1 và 5 nên cho 0, điều này cho 1

(Để bảo vệ nỗ lực cuối cùng của tôi, tôi không bao giờ nên cố gắng trả lời có lý do trong khi tâm trí tôi nói với tôi rằng tôi đã trễ 2 tiếng để ngủ)


19

Cơ hội hoàn hảo để sử dụng phương pháp mở rộng:

public static class Int32Methods
{
    public static int DivideByAndRoundUp(this int number, int divideBy)
    {                        
        return (int)Math.Ceiling((float)number / (float)divideBy);
    }
}

Điều này làm cho mã của bạn uber có thể đọc được:

int result = myInt.DivideByAndRoundUp(4);

1
Ơ, cái gì? Mã của bạn sẽ được gọi là myInt.DivideByAndRoundUp () và sẽ luôn trả về 1 ngoại trừ đầu vào bằng 0 sẽ gây ra Ngoại lệ ...
cấu hình

5
Thất bại sử thi. (-2) .DivideByAndRoundUp (2) trả về 0.
Timwi

3
Tôi thực sự đến bữa tiệc muộn, nhưng mã này có được biên dịch không? Lớp Toán của tôi không chứa phương thức Trần mà có hai đối số.
R. Martinho Fernandes

17

Bạn có thể viết một người trợ giúp.

static int DivideRoundUp(int p1, int p2) {
  return (int)Math.Ceiling((double)p1 / p2);
}

1
Vẫn cùng một số lượng diễn viên diễn ra
ChrisF

29
@Outlaw, giả sử tất cả những gì bạn muốn. Nhưng đối với tôi nếu họ không đặt câu hỏi, tôi thường cho rằng họ không xem xét nó.
JaredPar

1
Viết một người trợ giúp là vô ích nếu chỉ có vậy. Thay vào đó, hãy viết một người trợ giúp với một bộ kiểm tra toàn diện.
heo

3
@dolmen Bạn có quen thuộc với khái niệm tái sử dụng không? oO
Rushyo

4

Bạn có thể sử dụng một cái gì đó như sau.

a / b + ((Math.Sign(a) * Math.Sign(b) > 0) && (a % b != 0)) ? 1 : 0)

12
Mã này rõ ràng là sai theo hai cách. Trước hết, có một lỗi nhỏ trong cú pháp; bạn cần thêm dấu ngoặc đơn. Nhưng quan trọng hơn, nó không tính được kết quả mong muốn. Ví dụ: thử kiểm tra với a = -1000000 và b = 3999. Kết quả chia số nguyên thông thường là -250. Phân chia kép là -250,0625 ... Hành vi mong muốn là làm tròn số. Rõ ràng làm tròn chính xác từ -250,0625 là làm tròn đến -250, nhưng mã của bạn làm tròn lên -249.
Eric Lippert

36
Tôi rất tiếc phải tiếp tục nói điều này nhưng mã của bạn vẫn VẪN SAU Daniel. 1/2 nên làm tròn LÊN thành 1, nhưng mã của bạn làm tròn nó xuống còn 0. Mỗi lần tôi tìm thấy một lỗi bạn "sửa" nó bằng cách giới thiệu một lỗi khác. Lời khuyên của tôi: hãy ngừng làm điều đó. Khi ai đó tìm thấy lỗi trong mã của bạn, đừng chỉ sửa lỗi mà không suy nghĩ rõ ràng nguyên nhân gây ra lỗi ở nơi đầu tiên. Sử dụng thực hành kỹ thuật tốt; tìm lỗ hổng trong thuật toán và sửa nó. Lỗ hổng trong cả ba phiên bản thuật toán không chính xác của bạn là bạn không xác định chính xác khi làm tròn số "xuống".
Eric Lippert

8
Không thể tin được có bao nhiêu lỗi trong đoạn mã nhỏ này. Tôi không bao giờ có nhiều thời gian để nghĩ về nó - kết quả thể hiện trong các bình luận. (1) a * b> 0 sẽ đúng nếu nó không tràn. Có 9 kết hợp cho dấu của a và b - [-1, 0, +1] x [-1, 0, +1]. Chúng ta có thể bỏ qua trường hợp b == 0 để lại 6 trường hợp [-1, 0, +1] x [-1, +1]. a / b làm tròn về 0, làm tròn cho kết quả âm tính và làm tròn xuống cho sự phục hồi tích cực. Do đó việc điều chỉnh phải được thực hiện nếu a và b có cùng dấu và không phải là cả hai.
Daniel Brückner

5
Câu trả lời này có lẽ là điều tồi tệ nhất tôi đã viết trên SO ... và bây giờ nó được liên kết bởi blog của Eric ... Chà, ý định của tôi là không đưa ra một giải pháp dễ đọc; Tôi đã thực sự khóa cho một hack ngắn và nhanh chóng. Và để bảo vệ giải pháp của tôi một lần nữa, tôi đã có ý tưởng ngay lần đầu tiên, nhưng không nghĩ đến việc tràn ra. Rõ ràng là sai lầm của tôi khi đăng mã mà không viết và kiểm tra nó trong VisualStudio. Các "sửa lỗi" thậm chí còn tồi tệ hơn - tôi đã không nhận ra rằng đó là một vấn đề tràn và nghĩ rằng tôi đã phạm sai lầm logic. Do đó, "bản sửa lỗi" đầu tiên không thay đổi gì cả; Tôi vừa đảo ngược
Daniel Brückner

10
logic và đẩy các lỗi xung quanh. Ở đây tôi đã phạm sai lầm tiếp theo; như Eric đã đề cập, tôi không thực sự phân tích lỗi và chỉ làm điều đầu tiên có vẻ đúng. Và tôi vẫn không sử dụng VisualStudio. Được rồi, tôi đã vội vàng và đã không dành nhiều hơn năm phút cho "sửa chữa", nhưng đây không phải là một cái cớ. Sau khi tôi Eric liên tục chỉ ra lỗi, tôi đã kích hoạt VisualStudio và tìm ra vấn đề thực sự. Khắc phục bằng cách sử dụng Sign () làm cho mọi thứ trở nên khó đọc hơn và biến nó thành mã mà bạn không thực sự muốn duy trì. Tôi đã học được bài học của mình và sẽ không còn đánh giá thấp sự khó khăn như thế nào
Daniel Brückner

-2

Một số câu trả lời trên sử dụng phao, điều này không hiệu quả và thực sự không cần thiết. Đối với ints không dấu, đây là một câu trả lời hiệu quả cho int1 / int2:

(int1 == 0) ? 0 : (int1 - 1) / int2 + 1;

Đối với ints đã ký, điều này sẽ không chính xác


Không phải những gì OP yêu cầu ở nơi đầu tiên, không thực sự thêm vào các câu trả lời khác.
santamanno

-4

Vấn đề với tất cả các giải pháp ở đây là họ cần diễn viên hoặc họ có vấn đề về số. Đúc để nổi hoặc gấp đôi luôn là một lựa chọn, nhưng chúng ta có thể làm tốt hơn.

Khi bạn sử dụng mã của câu trả lời từ @jerryjvl

int div = myInt1 / myInt2;
if ((div >= 0) && (myInt1 % myInt2 != 0))
    div++;

có lỗi làm tròn số. 1/5 sẽ làm tròn lên, vì 1% 5! = 0. Nhưng điều này là sai, vì làm tròn sẽ chỉ xảy ra nếu bạn thay 1 bằng 3, vì vậy kết quả là 0,6. Chúng ta cần tìm cách làm tròn lên khi phép tính cho chúng ta một giá trị lớn hơn hoặc bằng 0,5. Kết quả của toán tử modulo trong ví dụ trên có phạm vi từ 0 đến myInt2-1. Việc làm tròn sẽ chỉ xảy ra nếu phần còn lại lớn hơn 50% số chia. Vì vậy, mã điều chỉnh trông như thế này:

int div = myInt1 / myInt2;
if (myInt1 % myInt2 >= myInt2 / 2)
    div++;

Tất nhiên chúng tôi cũng có một vấn đề làm tròn tại myInt2 / 2, nhưng kết quả này sẽ cung cấp cho bạn một giải pháp làm tròn tốt hơn so với các giải pháp khác trên trang web này.


"Chúng tôi cần tìm cách làm tròn số khi phép tính cho chúng tôi giá trị lớn hơn hoặc bằng 0,5" - bạn đã bỏ lỡ điểm của câu hỏi này - hoặc luôn làm tròn nghĩa là OP muốn làm tròn 0,001 đến 1.
Grhm
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.