정수 나누기가 항상 반올림되도록하려면 어떻게해야합니까?


242

필요한 경우 정수 나누기가 항상 반올림되도록하고 싶습니다. 이것보다 더 좋은 방법이 있습니까? 진행중인 캐스팅이 많이 있습니다. :-)

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

48
"더 나은"것으로 간주되는 것을 더 명확하게 정의 할 수 있습니까? 빨리? 더 짧습니까? 더 정확한? 더 강력합니까? 더 분명합니까?
Eric Lippert

6
C #에서는 항상 수학을 많이 사용합니다. 따라서 이런 종류의 언어에는 적합하지 않습니다. -3.1은 -3 (위로) 또는 -4 (제로에서 멀어짐)로 이동해야합니다.
Keith

9
에릭 : "더 정확하다? 더 강력하다? 더 분명하다"는 것은 무슨 뜻입니까? 실제로 내가 의미 한 것은 단지 "더 나은"것입니다. 독자들이 더 나은 의미를 갖도록하겠습니다. 누군가가 더 짧은 코드 조각을 가지고 있다면, 다른 코드가 더 빠르고 훌륭하다면 :-) 어떤 제안이 있습니까?
Karsten

1
그래도 제목을 읽을 때 "아, C #의 일종의 정리일까요?"
Matt Ball

6
이 질문이 미묘하게 어려워지고 토론이 얼마나 유익했는지는 정말 놀랍습니다.
저스틴 모건

답변:


669

업데이트 :이 질문은 2013 년 1 월 내 블로그의 주제였습니다 . 좋은 질문 감사합니다!


정수 산술을 올바르게 얻는 것은 어렵습니다. 지금까지 충분히 입증 된 것처럼 "영리한"트릭을 시도하는 순간 실수를 저지른 가능성은 높습니다. 결함이 발견되면 수정 사항이 다른 부분을 위반하는지 여부를 고려하지 않고 결함을 수정하도록 코드를 변경 것은 좋은 문제 해결 기술이 아닙니다. 지금까지 우리는이 완전히 문제가 아닌 어려운 문제에 대한 5 가지의 잘못된 정수 산술 솔루션을 게시했다고 생각했습니다.

정수 산술 문제에 접근하는 올바른 방법, 즉 처음에 정답을 얻을 가능성을 높이는 방법은 문제를주의 깊게 접근하고 한 번에 한 단계 씩 해결하며 올바른 엔지니어링 원칙을 사용하는 것입니다. 그래서.

교체하려는 사양을 읽으십시오. 정수 나누기 사양에는 다음과 같은 내용이 명시되어 있습니다.

  1. 나누기는 결과를 0으로 반올림합니다

  2. 두 피연산자가 동일한 부호를 갖는 경우 결과는 0 또는 양수이고 두 피연산자가 반대 부호를 갖는 경우 결과는 0 또는 음수입니다

  3. 왼쪽 피연산자가 표현할 수있는 가장 작은 int이고 오른쪽 피연산자가 –1이면 오버플로가 발생합니다. [...] [anarithmeticException]이 발생하는지 아니면 결과 값이 왼쪽 피연산자의 값으로 오버 플로우되지 않는지에 대해 구현 정의됩니다.

  4. 오른쪽 피연산자의 값이 0이면 System.DivideByZeroException이 발생합니다.

우리가 원하는 것은 결과를 몫을 계산하지만 반올림 정수 분할 기능입니다 항상 위쪽으로 하지, 0에 가까워 항상 .

따라서 해당 기능에 대한 사양을 작성하십시오. 우리의 함수 int DivRoundUp(int dividend, int divisor)는 가능한 모든 입력에 대해 동작을 정의해야합니다. 정의되지 않은 동작은 매우 걱정되므로 제거하십시오. 우리는 우리의 작업이 다음 사양을 가지고 있다고 말할 것입니다 :

  1. 제수가 0 인 경우 연산이 발생합니다.

  2. 배당이 int.minval이고 제수가 -1이면 연산이 발생합니다.

  3. 나머지가없는 경우-나눗셈이 '짝수'인 경우 반환 값은 정수 몫입니다

  4. 그렇지 않으면 몫 보다 큰 가장 작은 정수 , 즉 항상 반올림합니다.

이제 우리는 사양을 가지고 있으므로 테스트 가능한 디자인을 만들 수 있음을 알고 있습니다. . "double"해답이 문제 설명에서 명시 적으로 거부 되었기 때문에 몫을 double로 계산하지 않고 정수 산술로만 문제를 해결한다는 추가 설계 기준을 추가한다고 가정합니다.

그래서 우리는 무엇을 계산해야합니까? 정수 산술만으로 사양을 충족하려면 세 가지 사실을 알아야합니다. 첫째, 정수 몫은 무엇입니까? 둘째, 부서에 나머지 부분이 없었습니까? 그리고 세 번째가 아닌 경우 정수 몫은 반올림 또는 내림으로 계산 되었습니까?

이제 사양과 디자인을 갖추 었으므로 코드 작성을 시작할 수 있습니다.

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

이것이 영리합니까? 아뇨? 아니요. 짧습니까? 사양에 따라 맞습니까? 나는 그렇게 생각하지만 완전히 테스트하지는 않았습니다. 그래도 꽤 좋아 보인다.

우리는 여기 전문가입니다. 좋은 엔지니어링 사례를 사용하십시오. 도구를 연구하고, 원하는 동작을 지정하고, 오류 사례를 먼저 고려한 다음, 정확한 정확성을 강조하기 위해 코드를 작성하십시오. 그리고 버그를 발견하면 무작위로 비교 방향을 바꾸고 이미 작동하는 것을 중단하기 전에 알고리즘이 결함이 있는지 여부를 고려하십시오.


44
탁월한 모범 답변
Gavin Miller

61
내가 신경 쓰는 것은 행동이 아닙니다. 두 행동 모두 정당한 것 같습니다. 내가 신경 쓰는 것은 지정 되지 않았기 때문에 쉽게 테스트 할 수 없다는 것입니다. 이 경우 자체 연산자를 정의하므로 원하는 동작을 지정할 수 있습니다. 나는 그 행동이 "투척"인지 "투척하지"않는지는 신경 쓰지 않지만, 그것이 언급되도록주의한다.
Eric Lippert

68
젠장, pedantry 실패 :(
Jon Skeet

32
Man-저 책 좀 써 주실 래요?
xtofl

76
@finnw : 테스트했는지 여부는 관련이 없습니다. 이 정수 산술 문제를 해결하는 것은 비즈니스 문제 가 아닙니다 . 그렇다면 테스트 중입니다. 누군가가 자신의 비즈니스 문제를 해결하기 위해 인터넷으로 낯선 사람의 코드를 가지고 싶은 경우 무거운 짐에 그들을 철저하게 테스트합니다.
Eric Lippert

49

지금까지의 모든 대답은 다소 복잡해 보입니다.

C # 및 Java에서 긍정적 인 배당 및 제수의 경우 다음을 수행하면됩니다.

( dividend + divisor - 1 ) / divisor 

출처 : 숫자 변환, 롤랜드 백 하우스, 2001


대박. 모호성을 제거하기 위해 괄호를 추가해야하지만, 그 증거는 약간 길었지만 장에서 그것을 볼 수 있다고 생각할 수 있습니다.
Jörgen Sigvardsson

1
흠 ... 배당 = 4, 제수 = (-2)는 어떻습니까 ??? 4 / (-2) = (-2) = (-2) 반올림 후. 그러나 반올림 한 후 (4 + (-2)-1) / (-2) = 1 / (-2) = (-0.5) = 0 알고리즘을 제공했습니다.
Scott

1
@ Scott-죄송합니다.이 솔루션은 긍정적 인 배당금과 약수에만 해당됩니다. 이것을 명확히하기 위해 답변을 업데이트했습니다.
이안 넬슨

1
물론,이 접근법의 부산물로 분자에 다소 인공적인 오버플로가 발생할 수 있습니다.
TCC

2
@PIntag : 아이디어는 좋지만 모듈로를 잘못 사용했습니다. 13과 3을 가져옵니다. 예상 결과 5이지만 ((13-1)%3)+1)결과는 1입니다. 올바른 종류의 나누기를 1+(dividend - 1)/divisor하면 긍정적 인 배당과 제수에 대한 답과 같은 결과가 나타납니다. 또한 오버플로 문제는 없지만 인위적인 문제 일 수 있습니다.
Lutz Lehmann

48

최종 int 기반 답변

부호있는 정수의 경우 :

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

부호없는 정수의 경우 :

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

이 답변에 대한 추론

정수 나누기 ' /'는 0 (사양의 7.7.2)으로 반올림하도록 정의되어 있지만 반올림하려고합니다. 즉, 부정적인 답변은 이미 올바르게 반올림되지만 긍정적 인 답변은 조정해야합니다.

0이 아닌 양수 대답은 쉽게 검색 할 수 있지만 음수 값의 반올림 또는 양수 값의 반올림 일 수 있으므로 답 0은 조금 까다 롭습니다.

가장 안전한 방법은 두 정수의 부호가 모두 같은지 확인하여 정답이 언제 있어야하는지 감지하는 것입니다. 이 ^경우 두 값의 정수 xor 연산자 ' '는 음수가 아닌 결과를 의미하는 0 부호 비트가되므로 (a ^ b) >= 0반올림 전에 결과가 양수 여야한다고 확인합니다. 또한 부호없는 정수의 경우 모든 대답은 분명히 긍정적 이므로이 검사는 생략 할 수 있습니다.

남은 유일한 검사는 반올림이 발생했는지 여부뿐입니다 a % b != 0.

교훈

산술 (정수 또는 기타)은 생각만큼 간단하지 않습니다. 항상 신중하게 생각해야합니다.

또한 내 최종 답변이 부동 소수점 답변만큼 '단순'또는 '분명한'또는 '빠른'것도 아니지만, 그것은 나에게 매우 강력한 구속력을 가지고 있습니다. 나는 이제 대답을 통해 추론을 했으므로 실제로는 그것이 정확하다는 것을 확신합니다 ( 에릭의 지시에 따라 다른 사람이 다르게 말해 줄 때까지 ).

부동 소수점 답변에 대해 동일한 확신을 갖기 위해서는 부동 소수점 정밀도가 방해가 될 수있는 조건이 있는지 여부와 Math.Ceiling아마도 가능합니까? '올바른'입력에서 바람직하지 않은 것.

여행 한 길

(필자는 두 번째 교체 교체 참고 myInt1로를 myInt2그건 당신이 무엇을 의미하는지이었다 가정) :

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

와:

(myInt1 - 1 + myInt2) / myInt2

myInt1 - 1 + myInt2사용하는 정수 유형을 오버플로하면 예상 한 결과를 얻지 못할 수 있다는 유일한 경고 입니다.

이유가 잘못되었습니다 : -1000000 및 3999는 -250을 제공해야하며, 이것은 -249를 제공합니다

편집 :
음수 myInt1값에 대한 다른 정수 솔루션과 동일한 오류 가 있다고 생각하면 다음과 같은 작업이 더 쉬울 수 있습니다.

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

div정수 연산 만 사용 하면 올바른 결과를 얻을 수 있습니다.

이것이 잘못된 이유 : -1 및 -5는 1을 제공해야하며, 이것은 0을 제공합니다

편집 (한 번 더, 느낌으로) :
나누기 연산자가 0으로 반올림합니다. 부정적인 결과의 경우 이것은 정확하기 때문에 음이 아닌 결과 만 조정하면됩니다. 또한 점을 고려 DivRem만이 작업을 수행 /하고, %어쨌든,이 전화를 건너 (그리고 그것이 필요하지 않을 때 피하기 모듈로 계산에 쉽게 비교로 시작)하자 :

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

이유가 잘못되었습니다 : -1과 5는 0을 주어야합니다.

(마지막 시도에 대한 내 자신의 방어에서, 나는 내 마음이 내가 2 시간 늦게 잠 들었다고 말하는 동안 결코 합리적인 대답을 시도하지 않았어야했다)


19

확장 방법을 사용할 수있는 완벽한 기회 :

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

이렇게하면 코드를 읽을 수있게됩니다.

int result = myInt.DivideByAndRoundUp(4);

1
음, 뭐? 코드는 myInt.DivideByAndRoundUp ()이며 0을 입력하는 경우를 제외하고는 항상 1을 반환합니다. 예외는 다음과 같습니다.
구성자

5
에픽 실패. (-2) .DivideByAndRoundUp (2)는 0을 반환합니다.
Timwi

3
나는 정말로 파티에 늦었지만이 코드는 컴파일됩니까? 내 Math 클래스에는 두 가지 인수를 사용하는 Ceiling 메서드가 없습니다.
R. Martinho Fernandes

17

도우미를 작성할 수 있습니다.

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

1
그래도 같은 양의 캐스팅이 진행되고 있음
ChrisF

29
@ 무법자, 당신이 원하는 모든 것을 추정합니다. 그러나 그들이 질문에 그것을 넣지 않으면, 나는 일반적으로 그들이 그것을 고려하지 않았다고 가정합니다.
JaredPar

1
그냥 도우미라면 글쓰기는 쓸모가 없습니다. 대신 종합적인 테스트 스위트로 헬퍼를 작성하십시오.
고인돌

3
@dolmen 코드 재사용 개념에 익숙 하십니까? oO
Rushyo

4

다음과 같은 것을 사용할 수 있습니다.

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

12
이 코드는 분명히 두 가지 방식으로 잘못되었습니다. 우선, 구문에 사소한 오류가 있습니다. 더 많은 괄호가 필요합니다. 그러나 더 중요한 것은 원하는 결과를 계산하지 않습니다. 예를 들어, a = -1000000 및 b = 3999로 테스트 해보십시오. 정규 정수 나누기 결과는 -250입니다. 이중 나누기는 -250.0625입니다 ... 원하는 동작은 반올림하는 것입니다. 분명히 -250.0625의 올바른 반올림은 -250으로 올림되지만 코드는 -249로 올림됩니다.
Eric Lippert

36
계속해서 유감스럽게 생각하지만 코드는 여전히 Daniel입니다. 1/2은 1로 반올림해야하지만 코드는 0으로 반올림합니다. 버그를 찾을 때마다 다른 버그를 소개하여 "수정"합니다. 내 충고 : 그만해. 누군가 코드에서 버그를 발견하면 처음에 버그를 일으킨 원인을 명확하게 생각하지 않고 수정 사항을 함께 쓰지 마십시오. 좋은 엔지니어링 사례를 사용하십시오. 알고리즘에서 결함을 찾아서 수정하십시오. 알고리즘의 세 가지 잘못된 버전의 결함은 반올림이 "다운 된"시기를 정확하게 결정하지 않았다는 것입니다.
Eric Lippert

8
이 작은 코드에 몇 개의 버그가 있는지 믿을 수 없습니다. 나는 그것에 대해 생각할 시간이 없었습니다. 결과는 의견에 나타납니다. (1) 넘치지 않으면 a * b> 0이 정확합니다. a와 b의 부호-[-1, 0, +1] x [-1, 0, +1]의 9 가지 조합이 있습니다. 6 개의 경우 [-1, 0, +1] x [-1, +1]을 남기고 b == 0을 무시할 수 있습니다. a / b는 0으로 반올림됩니다. 즉, 음수 결과는 반올림되고 양수는 반올림됩니다. 따라서 a와 b의 부호가 같고 0이 아닌 경우 조정을 수행해야합니다.
Daniel Brückner

5
이 답변은 아마도 내가 쓴 최악의 것입니다 ... 그리고 이제 Eric의 블로그에 연결되어 있습니다 ... 글쎄, 내 의도는 읽을 수있는 솔루션을 제공하지는 않았습니다. 나는 짧고 빠른 해킹을 위해 정말로 잠겨있었습니다. 그리고 솔루션을 다시 방어하기 위해 처음에는 아이디어를 얻었지만 오버플로에 대해서는 생각하지 않았습니다. VisualStudio에서 코드를 작성하고 테스트하지 않고 코드를 게시하는 것은 저의 실수였습니다. "수정"이 더 나빠졌습니다. 오버플로 문제라는 것을 알지 못했고 논리적 실수를했다고 생각했습니다. 결과적으로 첫 번째 "수정"은 아무 것도 변경하지 않았습니다. 방금 거꾸로
Daniel Brückner

10
논리와 버그를 밀었다. 여기서 나는 다음 실수를했다; Eric이 이미 언급했듯이 나는 실제로 버그를 분석하지 않았으며 첫 번째로 올바르게 보이는 것만 수행했습니다. 그리고 나는 여전히 VisualStudio를 사용하지 않았습니다. 좋아, 나는 서둘러서 "수정"에 5 분 이상을 쓰지 않았지만, 이것은 변명해서는 안된다. Eric이 버그를 반복해서 지적한 후 VisualStudio를 시작하여 실제 문제를 발견했습니다. Sign ()을 사용하여 수정하면 내용을 더 읽을 수 없게 만들고 실제로 유지 관리하지 않으려는 코드로 바꿉니다. 나는 나의 교훈을 배웠고 더 이상 까다로워지지 않을 것입니다
Daniel Brückner

-2

위의 답변 중 일부는 플로트를 사용하므로 비효율적이며 실제로는 필요하지 않습니다. 부호없는 정수의 경우 이것은 int1 / int2에 대한 효율적인 답변입니다.

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

부호있는 정수의 경우 올바르지 않습니다.


OP가 처음에 요구 한 것이 아니라 실제로 다른 답변에 추가하지는 않습니다.
santamanno

-4

여기에있는 모든 솔루션의 문제는 캐스트가 필요하거나 숫자 문제가 있다는 것입니다. 플로팅 또는 더블 캐스팅은 항상 선택 사항이지만 더 잘할 수 있습니다.

@jerryjvl의 답변 코드를 사용할 때

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

반올림 오류가 있습니다. 1 / 5는 1 % 5! = 0이기 때문에 반올림됩니다. 그러나 1을 3으로 바꾸면 반올림되므로 0.6이됩니다. 계산에 0.5 이상의 값을 줄 때 반올림 할 방법을 찾아야합니다. 위 예제에서 모듈러스 연산자의 결과 범위는 0에서 myInt2-1입니다. 반올림은 나머지가 제수의 50 %보다 큰 경우에만 발생합니다. 따라서 조정 된 코드는 다음과 같습니다.

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

물론 우리는 myInt2 / 2에서도 반올림 문제가 있지만이 결과는이 사이트의 다른 반올림 솔루션보다 더 나은 반올림 솔루션을 제공합니다.


"계산 값에 0.5 이상의 값을 주면 반올림 할 방법을 찾아야합니다."-이 질문의 요점을 놓쳤거나 항상 반올림합니다. 즉, OP는 0.001에서 1로 올림하려고합니다.
Grhm
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.