두 정수의 평균을 구하는 가장 좋은 방법은 무엇일까요?
물론, 프로그래밍 언어에서 계산하는 경우를 말합니다.
사실
http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html
이 글을 읽다가 문득 궁금해진 내용입니다.
양수, 음수 포함하여 모든 정수를 고려할 때, 어떤 방법이 가장 좋을까요?
아래 나열한 방법은 전부 위의 링크에서 가져와서, 제 생각을 덧붙인 것입니다.
가장 무식한 방법은
mid = (low + high) / 2;
입니다.
그런데 이 방법은 overflow의 문제가 있습니다.
그 다음 방법은
mid = low + (high - low) / 2;
입니다.
이 방법은 0 <= low <= high일 때는 잘 작동하지만,
high = 0, low = 1이 되면 (-1) / 2가 계산되면서 문제가 되고,
high = 0, low = INT_MIN이 되면 overflow가 발생합니다.
또 다른 방법은
mid = (((unsigned int) low) + ((unsigned int) high)) / 2;
입니다.
그런데, 이 방법도 문제가 있습니다.
원래 low, high가 음수였을 경우엔 오답이 나옵니다.
그리고 양수로 한정하더라도 INT_MAX * 2 <= UINT_MAX가 보장이 되는지는 의문입니다. (임의의 언어, 임의의 환경을 고려할 때 말입니다.)
또 다른 방법은
mid = (low / 2) + (high / 2) + (((low % 2) + (high % 2)) / 2);
입니다.
아마, X == ((X / 2) * 2) + (X % 2) for all X 정도의 조건이 필요한데, (아마도) 요새 언어에서 대부분 이 조건은 만족하니까 상관 없습니다.
그외에 필요한 조건은 발견하지 못했습니다.
다만, 연산의 수가 너무 많아서 사용하고 싶은 마음이 안 듭니다.
(이론적으로 /와 %는 연산 한 번에 될 듯 싶지만, 그런 명령어가 있는지 모르겠습니다. 그래서 연산이 많습니다.)
또 다른 방법은
mid = (low / 2) + (high / 2) + (low & high & 1);
입니다.
바로 위의 방법과 비슷한 접근입니다.
고민해본 결과, (-3) / 2를 -1로 계산하는 C에서는 오답이 나옵니다. (-3) / 2를 -2로 계산하면 (아마도) 언제나 정답인 것 같습니다.
gcc는 >>를 arithmetic shift로 계산하므로 써 먹을 수 있을 것 같습니다.
또 다른 방법은
mid = (low & high) + (low ^ high) / 2;
입니다.
이건 아무리 생각해도 항상 정답이 나오는 것 같습니다.
(-3) / 2를 -2로 계산하든 -1로 계산하든 상관 없습니다.
(low ^ high)가 음수이고 평균값이 정수가 아닐 때, 정확히 중간에서 0.5만큼 왼쪽을 찍냐 오른쪽을 찍냐 차이입니다.
게다가 정답을 보장하는 방식 중 비교적 연산 횟수도 적어 보입니다.
(코멘트가 없으면 이해하기 어렵다는 것이 단점일까요.)
과연 더 좋은 방법이 있을까요?
마지막 방법은 좋은 코드일까요? 정말 모든 경우에 동작한다고 확신할 수 있을까요?
이미 나온 연산을 비트연산으로 바꿔보면 좀 빨라질 것
이미 나온 연산을 비트연산으로 바꿔보면 좀 빨라질 것 같은데요
mid = (low / 2) + (high / 2) + (((low % 2) + (high % 2)) / 2);
이걸 다음과 같이 바꿔봅니다.
m = mid
l = low
h = high
m = (l>>1)+(h>>1) - (l-(l>>1)<<1)+(h - ((h>>1)<<1))>>1
뭐...연산 수는 같거나 더 많아보이네요 -_-;
피할 수 있을때 즐겨라! http://melotopia.net/b
((low % 2) + (high % 2)) / 2 이건 비트연산으로 간단히 될듯..
( low & 1 ) & (high & 1) 하면 되지 않나요?? 둘다 마지막비트가 1일때만 1이 나오게끔요..
low & high & 1
low & high & 1
음.. 저는 보통 두번째를 씁니다만...
수치가 안 크면 첫번째...
두번째에서 overflow가 발생할 수 있다는 인식은 이제껏 못했네요. Bit 연산은 하고 싶지 않고, 4번째 방법을 쓸 바에는 두번재 방법에 if else 를 적용하겠습니다.
그리고 몫과 나머지를 동시에 구하는 것은 Assembly에서 가능하고 C 함수에도 있습니다. 구조체로 몫과 나머지가 동시에 반환되죠.
만만하게 보고 풀어보려 했는데 어렵네요..;;
만만하게 보고 풀어보려 했는데 어렵네요..;;
오버플로우를 감안하고 있는것 자체가 특정 언어에 너무
오버플로우를 감안하고 있는것 자체가 특정 언어에 너무 의존적인 생각 아닌가 싶습니다.
c에서야 a+b가 경우에 따라 오버플로우가 날수 있겟지만
루비와 같이 큰 정수를 다루는 방법을 이미 내장한 언어의 경우는 오버플로우가 날래야 날수가 없죠.
(메모리가 변수가 되겟지만 말입니다.)
(그럴리도 없겟지만)극단적인 속도가 필요한 경우가 아니라면
가장 직관적인 방법인 (a+b)/2 와 같은 방법이 제일 좋은 방법 같습니다.
누가봐도 저건 두 값의 평균을 구하는 연산이니까요.
C나 C++같은 언어만 쓰는 경우도 있으니까요.
C나 C++같은 언어만 쓰는 경우도 있으니까요. 오버플로우를 언제나 무시할 수는 없는 것 같습니다.
물론, 좋은 언어를 쓰면 (a + b) / 2가 직관적으로 좋은 방법입니다.
정확성, 이식성, 성능, 가독성을 동시에 만족시키는 것은 어려운 문제인 것 같군요.
개인적으로 저라면,
mid = (low & high) + (low ^ high) >> 1;
mid = (low & high) + (low ^ high) / 2;
중 하나를 고르겠습니다. 특히
mid = (low & high) + (low ^ high) >> 1;
가 자세히 들여다 보면 참 잘 만든 코드네요.
다만, >>를 쓸 경우, C 언어 등 signed type에 대한 bit shift가 정의되지 않은 언어에서는, portability는 문제가 됩니다.
begin{signature}
THIS IS SPARTA!!!!!n.
end{signature}
댓글 달기