|
재미있는 글을 보고서.. 나도 포스팅 알다시피, 비트연산자 << n 은 n 비트 만큼 왼쪽으로, >> n 은 n 비트만큼 오른쪽으로 비트를 이동(shift) 한다는 의미이다. 예를 들어, int n = 1; n =<< 1; 위와 같이 하면 n 의 값은 1 비트씩 왼쪽으로 밀려서 답이 2 가 된다. ( 일반적으로 2^n 값을 곱하거나 나눌 때, 비트 연산자를 종종 활용한다. 비트연산의 장점은 비트 단위에서 직접 조작하므로 일반 사칙연산보다 더 빠르다는 것.. 단, 이러한 연산법은 양수일 경우에만 제대로 동작한다. 자세한 내용은 아래 다시 설명 ) 그러다면 아래와 같은 비트 연산의 결과는 어떻게 될까? ( 테스트 환경은 Intel 계열 CPU ) Quiz 1) int n = 1; n =<< 31; cout << n << endl // n 은 얼마?? Quiz 2) int n = 1; n =<< 32; cout << n << endl // n 은 얼마?? Quiz 3) int n = 1; n =<< 33; cout << n << endl // n 은 얼마?? Quiz 4) int n = -2; n =<< 31; cout << n << endl // n 은 얼마?? Quiz 5) int n = -256; n =>> 31; cout << n << endl // n 은 얼마?? Quiz 6) int n = -256; n =>> 33; cout << n << endl // n 은 얼마?? Quiz 7) int n = 1; n <<= -123; cout << n << endl // n 은 얼마?? C99 표준문서 의 6.5.7 Bitwise shift operators 를 보면 다음과 같이 정의되어 있다. If the value of the right operand is negative or is greater than or equal to the width of the promoted left operand, the behavior is undefined. The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1 * 2^E2, reduced modulo one more than the maximum value representable in the result type. If E1 has a signed type and nonnegative value, and E1 * 2^E2 is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined. The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of E1 / 2^E2. If E1 has a signed type and a negative value, the resulting value is implementation-defined. 우측 피연산자 값이 음수이거나 좌측 피연산자의 범위보다 큰 경우의 행위는 정의되지 않았다 (undefined) E1 이 양수인 경우에는 E1 << E2 는 E1 을 E2 비트만큼 왼쪽으로 이동, 이 것은 E1 에 2^E2 을 곱한 것과 같다. E1 이 음수인 경우의 동작은 정의되지 않았다. E1 이 양수인 경우 E1 >> E2 역시 E1 을 E2 비트만큼 오른쪽으로 이동, 이것은 E2 를 2^E2 를 나눈 것과 같다. 그러나, E1 이 음수인 경우, 결과는 구현에 따라 달라진다 (implementation-defined) 결국, C99 표준에 정의되지 않은 사항은 CPU 명령어 정의에 따라 달라지게 되는데... Intel CPU 에서의 동작은 다음과 같다. 우선, << , >> 연산자의 오른쪽 피연산자는 short 일때 4 자리(0 ~ 15까지), int 일때 5 자리(0 ~ 31 까지), 64 비트형인 long long 일때 6 자리 (0 ~ 63 까지) 의 범위에 대해서만 유효하다. 다시 말하면 오른쪽 피연산자는 short 일때 하위 4 비트만 사용하며, int 일때 하위 5 비트만 사용하며, long long 일때 하위 6 비트만 사용한다. 쉬프트 연산은 왼쪽 피연산자 값의 범위 (비트 길이) 내에서만큼만 쉬프트 연산이 이루어진다. 즉, Quiz 2 의 n <<= 32 에서 32 는 100000 이 되고, 하위 5 비트는 00000 이므로 n <<= 32 는 n <<= 0 과 같다. Quiz 3 의 n <<= 33 는 33 이 100001 이며 이중 하위 5 비트는 00001 이므로 n <<= 33 은 n <<= 1 과 같아지는 것이다. n << x 이면 일반적으로는 n 이 2^x 만큼 곱한 것과 같아지게 되는데, Quiz 1 의 경우는 1 << 31 의 답이 1 * 2^31 이 아닌 -2147483648 ( -1 * 2^31 ) 이 된다. 이렇게 되는 이유는 int 의 경우 최상위 비트가 부호 비트가 되어 부호 비트가 0 에서 1 로 바뀌면서 음수값을 취하게 되기 때문이다. ( 최상위비트 참고 ) Quiz 1 의 결과 00000000 00000000 00000000 00000001 // 1 을 10000000 00000000 00000000 00000000 // 31 만큼 좌측을 이동, 부호 비트가 1 이 되며 답이 -2147483648 만약 n 이 unsigned int 였다면, 답은 양의 정수 2147483648 이 된다. Quiz 2 의 결과 00000000 00000000 00000000 00000001 // 1 을 00000000 00000000 00000000 00000001 // <<= 32 는 <<= 0 과 같으므로, 결과는 변함 없어 답은 1 Quiz 3 의 결과 00000000 00000000 00000000 00000001 // 1 을 00000000 00000000 00000000 00000010 // <<= 33 는 <<= 1 과 같으므로, 결과는 1 비트 좌측이동하여 답은 2 Quiz 4 의 결과 11111111 11111111 11111111 11111110 // -2 을 00000000 00000000 00000000 00000000 // <<= 31 로 31 만큼 좌측 이동. 결과는 0 이 된다. 왼쪽 피연산자가 음수인 경우에 << 의 경우는 왼쪽으로 비트를 이동하면서 0 으로 채운다. 그런데, implementation-defined 되 있는 >> 연산자는 동작방식이 다르다. << 연산자의 경우 양수/음수 모두 이동한 자리를 0 으로 채운다. 반면에 >> 연산자의 경우 양수일 경우에는 오른쪽으로 비트를 이동하면서 이동한 자리는 0 으로 채우는데, 음수일 경우에는 이동한 자리를 1 로 채우게 된다. 그래서 Quiz 5 의 경우 오른쪽으로 이동하고 난 후의 남은 31개 비트를 모두 1 로 채우므로 답은 -1 이 된다. Quiz 5 의 결과 11111111 11111111 11111111 00000000 // -256 11111111 11111111 11111111 11111111 // 31 만큼 우측 이동, 음수의 경우 이동한 자리는 1 로 채워짐. 답은 -1 Quiz 6 의 결과 11111111 11111111 11111111 00000000 // -256 11111111 11111111 11111111 10000000 // 33 만큼 우측이동, >> 33 은 >> 1 과 같아서 답은 -128 >> 연산자 역시 << 연산자와 마찬가지로 int 의 경우 오른족 피연산자의 하위 5 비트에 해당하는 값만큼만 이동을 한다. 그러므로 int n 의 경우 n =>> 33 은 n=>> 1 과 같아진다. 음수일 경우 >> 연산을 하여 이동한 자리를 0 이 아닌 1 로 채우는 이유는 아마도 n =-2, n >>= 1 일때 n = -1 이 되는 것과 같이 음수인 경우 나눗셈 연산이 정상적으로 동작하도록 하게 하려는 의도였을 것이다. 하지만 음수는 2 의 보수로 계산되기 때문에 음수인 경우에는 n 비트 쉬프트 연산의 결과가 2^n 곱셉/나눗셈의 결과와는 동일하게 동작하지 않는 경우도 종종 생긴다. ( 2^n 꼴의 수가 아닌 경우 이러한 경우가 발생 ) 예를 들면 n = -10 일때 n =>> 3; 의 결과는 n /= 8; 과는 다른 결과가 나온다. ( n =>> 3 이면 n = -2, n /= 8 이면 n = -1 ) 11111111 11111111 11111111 11110110 // -10 11111111 11111111 11111111 11111110 // 우측으로 3 비트 이동한 후의 결과, -2 가 됨 끝으로, Quiz 7 과 같이 쉬프트 연산에 있어서 오른쪽 피연산자의 값이 음수일 경우 역시 오른쪽 피연산자의 하위 5 개 비트의 값에 해당하는 크기만큼만 비트 이동을 하므로, -123 이 11111111 11111111 11111111 10000101 이며 이중 하위 5 비트는 00101 이므로 결국 n <<= -123 은 n <<= 5 와 같다. 그러므로 1 <<= 5 한 결과는 32. 비트연산은 다양한 최적화 테크닉에 있어 중요한 부분을 차지하고 있고, 의외로 많은 사람들이 헷갈려 하는 부분이당( 나 포함 )... 'IT Story > Programming Language' 카테고리의 다른 글
|
BLOG ARTICLE Algorithm | 4 ARTICLE FOUND
- 2008/10/24 비트 이동 연산자 ( << , >> ) 의 결과
- 2008/10/15 최대공약수(GCD) 와 최소공배수(LCM) (9)
- 2008/09/12 퀵소트(Quick Sort) 의 시간복잡도(Time Complexity) (12)
- 2008/01/18 소수 구하기 (Finding Primes) 알고리즘 (6)
|
'Contest > Algorithm' 카테고리의 다른 글
|
|
오늘 알고리즘 수업을 듣다가 Time Complexity 계산방법에 대한 강의 중에 누군가 수업시간에 한 질문, "우리가 흔히 nlogn 정렬이라고 말하는 퀵 소트의 경우 Worst Case 는 O(n^2) 이 된다. 교수님이 설명하시길 알고리즘의 시작복잡도는 Worst Case 를 기준으로 측정한다고 얘기했는데 퀵소트는 Average Case 를 기준으로 할 때만 O(nlogn) 이 되는 것인데, 그렇다면 과연 퀵 소트를 O(nlogn) 정렬이라고 정렬이라고 부르는 것이 맞는가?" 참고로, 머지 소트(Merge Sort) 와 힙 소트(Heap Sort) 의 경우 Worst Case 에서도 O(nlogn) 에 수행된다. 당시 알고리즘의 Time Complexity 는 Worst Case 를 기준으로 측정한다고 설명하던 강사도 이 질문에 살짝 당황해서 좀 버벅이다가 실제 상황에서는 Average Case 를 대상으로 적용하는 경우가 많기 때문에 퀵 소트를 nlogn 정렬이라고 본다고 설명했다. 평소에 당연히 퀵 소트는 O(nlogn) 이라고만 생각하던 차, 이 질문을 듣고 나도 의문이 생겨서 집에 와서 CLRS 책을 다시 뒤져보았다. "Quicksort is a sorting algorithm whose worst-case running time is O(N^2) on an input array of n numbers, In spite of this slow worst-case running time, quicksort is often the best practical choice for sorting because it is remarkably efficient on the average: it's expected running time is O(nlogn), and the constant factors hidden in the O(nlon) notation are quite small. It also has the advantage of sorting in place and it works well even in memory environments. 그러니까, Quicksort 는 O(nlong) 에 숨어있는 상수 계수의 크기도 작고, 내부 정렬이며, 평균적으로 가장 빠르게 수행되는 정렬이며, 가상메모리 상에서도 잘 동작하는 일반적으로 가장 좋은 정렬이다는 이야기. partition 의 깊이 logn 과 각 partition 에서의 비교횟수 n 번이 수행되어 Average Case 는 O(nlogn), n-1 과 0 으로 partition 되는 경우 Worst case O(N^2) 이 된다. Average Case 는 대개 계산하기도 힘들고, 현실에서는 입력되는 값의 정확한 분포를 알기 어려우므로 쓰기 힘들다. Worst Case 는 알고리즘의 성능을 보여주는 척도가 될 뿐만 아니라 경험적으로 알고리즘의 수행시간은 Worst Case 인 경우의 수행시간과 큰 차이가 나지 않기때문에 일반적으로 알고리즘의 시간복잡도를 말할때는 Worst-Case 기준으로 평가한다. "어떤 알고리즘의 시간복잡도가 O(n^2) 이라면 이 알고리즘은 최악의 경우 n^2 만큼 수행한다." 는 의미가 된다. 결론적으로 강사가 이런 부분을 명확하게 설명을 안해준거네 -0- 'Contest > Algorithm' 카테고리의 다른 글
|
-
rubyeye
2008/09/16 09:29
댓글주소
수정/삭제
댓글쓰기
지나가다가 글 하나 올립니다 ㅋ
quick sort도 worst case O(nlogn)알고리즘이 맞습니다.
문제가 되는 것이 quick sort에서 각 단계마다 최적의 key값(중앙값)을 O(n)안에 찾을 수 있느냐인데
k번째 큰 값을 찾는 알고리즘이 O(n)이므로 가능합니다. 따라서 quicksort도 worst case O(nlogn)이 됩니다.
문제는 이런 식으로 key값을 찾으면 앞에 붙는 상수값이 커져서 practical하게는 더 느려진다는 점이죠...
즉, quicksort는 이론적으로 worst-case O(nlogn)입니다만, 실제로 사용할 때는 빠른 속도를 위해서
worst-case O(n^2)이 되더라도 대충(-_-; ) 키값을 찾게 된다는 거죠... 참고하시기 바랍니다 ^^ -
고글 2008/09/25 23:45 댓글주소 수정/삭제 댓글쓰기
형 저도 지나가다가.. 글 씁니다.
요점을 다 빗나게가 쓴 것 같아서..
Quick sort의 worst case는 O(N^2) 입니다. 오름차순 혹은 내림차순으로 정렬 되어 있을때 그렇게 되겠죠.
데이타를 많이 돌려보았을때 최악의 경우에서 Mergesort와 Heapsort 다 O(n log n)을 보장하지만
실제로 데이타를 돌려보면 Quick sort가 가장 빠른 결과값을 나오게 합니다.
Notation상 분명 Quicksort가 Mergesort나 Heapsort보다 확실히 느려야 하는데 그렇지 않는것은 하드웨어레벨에서 그문제가 발생하는거지요.
이유는 지역성(Locality) 특성 때문입니다.
"가상메모리 상에서도 잘 동작하는 일반적으로 가장 좋은 정렬이다는 이야기. "
이게 locality를 잘 반영했다는 얘긴데.
지역성에는 locality는 두가지가 있습니다.
시간지역성과 공간지역성이 있는데 여기에서는 공간지역성을 잘 반영했다라고 이해하시면됩니다.
시간지역성은 한번 나왔던 애가 다시 또 나올 가능성이 높다는것이고,
공간 지역성은 한번 발생한곳 주위에서 또 발생할 가능성이 높다는것입니다
Quicksort를 시뮬레이션 해서 잘 관찰해보면 pivot 주변에서 데이타의 위치 이동이 빈번히 발생하게 됩니다.
메모리에 데이타를 올리고, 캐쉬에 저장해놓고 쓸 때에, 메모리와 캐쉬 hit rate를 생각해보면 Quicksort가 빠릅니다.
- 컴구조책 메모리파트에서 이렇게 이유를 밝히고 있습니다.
P.S. 대학원준비한다고 책 처음부터 끝까지 다 봤더니, 몰랐었던 내용도 많이 알게 됐습니다. -
김대현 2008/10/22 22:01 댓글주소 수정/삭제 댓글쓰기
저기..질문좀 할께요
다항시간(다차시간: Polynomial-time) 라고 나오는데요
정의를 확실하게 잘 모르겠네요...
식으로 O(n^2)에서요 O는 뭐고 n은 무엇을 뜻하는 건가요??
정말 어렵네요..ㅡㅡ;;
답해주시면 정말 감사하겠습니다.
kdhenjoy@naver.com-
soyoja
2008/10/23 02:33
댓글주소
수정/삭제
O 는 Big-O 표기법입니다.
http://search.naver.com/search.naver?sm=tab_hty&where=nexearch&query=big-0+%C7%A5%B1%E2%B9%FD
검색을 해 보시거나 책을 찾아보시면 잘 나와있습니다.
n 이 의미하는 것은 정렬해야 하는 숫자의 갯수입니다.
-
-
김대현 2008/10/24 12:33 댓글주소 수정/삭제 댓글쓰기
감사합니다...근데요 또 궁금점이 있는데요~
O가 문제 걸리는데 최악의 시간이라는데
그 시간의 결정은 어떻게 하나요?
O표기법도 다양하던데..어떤때에 어떤걸 써야 할지...잘 모르겠네요...
NP Problem을 공부하다가 다항시간이 나와서 이리저리 알아보고 있는데
정말 어렵네요..ㅠㅠ
여기 BIG-O표기법은 식이 주어져 있을때 푸는게 맞나요??
아니면 문제가 주어졌을 때 식으로 전환해서 푸시나요?
학과 과목도 아니고 처음 배우는 거라서요..ㅠㅠ
쉽게 기초부터 설명 되어있는 책좀 추천좀 해주세요..ㅠㅠ
감사합니다 -
JM
2008/11/22 15:13
댓글주소
수정/삭제
댓글쓰기
제가 요즘 시간 복잡도 관련 챕터를 쓰면서 깨닫게 되었는데요, Worst Case 와 Big-O notation 이 밀접하게 연관되어 있어서 사람들이 착각하기 쉬울 뿐, 알고리즘의 시간 복잡도를 평가할 때 항상 최악의 경우를 기준으로 삼는 것은 아닙니다.
일단 잘 아시다시피 Big-O notation 은 어떤 함수가 주어질 때 그 함수의 상한을 표기하는 방법이죠. (상수 범위 내에서..) 그래서, 크기가 n 인 입력을 해결하기 위한 알고리즘의 수행 시간을 f(n) 라 할 때, f(n) = O(n^2) 이라고 쓰면 실제로 최악의 경우 알고리즘의 시간 복잡도를 나타내게 됩니다. 때문에, 사람들이 알고리즘의 수행 시간을 최악의 경우로 평가한다고 생각하기 쉽죠.
하지만, 실제로 Big-O notation 은 시간 복잡도의 표기를 간단하게 하기 위해 도입한 방법일 뿐, 알고리즘의 최악의 경우에 대한 시간을 측정하기 위해 도입한 방법은 아니죠. 따라서, Big-O 가 항상 '최악의 경우' 와 연관되어 있다는 직관은 잘못된 것이라고 생각해요.
퀵소트의 경우, 실제 실행 시간 f(n) 을 보는 것이 아니라 수행 시간의 기대값 (expected running time) g(n) 을 대신 보게 되지요. 이 때 f(n) 의 최고차항과 g(n) 의 최고차항이 각각 n^2 이고 nlgn 이기 때문에 최악의 경우엔 O(n^2), 기대값은 O(nlgn) 이라는 말은 완전히 정당한 것이라는... ^^
저도 한 때 "Big O 표기법은 최악의 경우를 표기하는 거라는데 왜 퀵소트가 nlgn 이지 -_-" 헷갈려 하던 때가 있었어서.. 사족을 덧붙여 봅니다.
덧) 구글에 worst case time complexity 를 쳐보니 이 블로그가 2번에 뜨더라고요. 그래서 와서 리플 달아 봤습니다. ^^;
덧2) 그리고 실제로 대부분 많은 알고리즘의 경우에는 expected running time 과 worst running time 의 order 가 같습니다. 머지소트나 힙소트의 경우에는 n 이 주어졌을 때 실행 시간 함수 f(n) 이 deterministic 하게 결정되기 때문이고.. 선형검색 같은 경우에도, 중간에 찾았을 경우 곧장 리턴하는 방법을 쓰더라도, expected time 은 n/2 이고 worst time 은 n 이니까, O(n) = O(n/2) 죠? ㅎㅎ
지하철에서 원고하다 장문의 리플을 달아 봤습니다 -_-; -
짱강 2008/12/27 01:11 댓글주소 수정/삭제 댓글쓰기
전산에서 최악의 경우인 빅O는 굉장히 중요한 의미를 시사한다.
빅O는 특정 행위를 수행하는데 특정 시간안에 수행됨을 보장함을 의미한다.
즉, 평균적인 타임이 N에 비례하는 것과 NlogN에 비례하는 두개의 알고리즘이 있다고 하자.
이로써는 해당 행위가 언제 끝날 수 있는지 보장되는 것은 아니다.
반면 최악의 경우 N^2과 NlogN에 비례한다고 하면
이는 해당 행위가 언제 끝날 수 있는지가 보장이 된다.
즉, 퀵 소트는 평균적으로 NlogN에 비례하는 다른 소트(힙, 병합, 기수)보다 상대적으로 빠를 수 있지만
최악의 경우(어느 정도 정렬되어 있는 자료 혹은 어느 정도 역순 정렬되어 있는 자료)의 경우 N^2에 비례하는 속도를 낸다.
즉, 자료가 어떤 성향이냐에 따라 수행 속도의 차이가 크다는 것이다.
결론적으로 퀵소트는 최악의 경우 NlogN에 비례하는 (힙, 병합,기수)정렬보다 빠르다고 얘기할 수 없다.
|
어떤 자연수 n 이 소수인지 구할때, n 이 작을 경우에는 다음과 같은 방법을 사용한다. Notes void trial(int end)
{ for( int a = 2; a <= end; a++ ) { if( a%2 == 0 && a != 2 ) { prime[a] = 0; continue; } bool isprime = true;
for( int b = 2; b*b <= a; b++ ) { if( a%b == 0 ) { isprime = false; break; } } if( isprime ) prime[a] = 1; } } 2. 에라토스테네스의 체(Sieve of Eratosthenes) void sieve(int end)
{ for( int a = 2; a*a <= end; a++ ) { if( prime[a] == 0 ) // 'a' is a prime { for( int b = a*2; b <= end; b += a ) prime[b] = 1; } } } 소수일 것 같은 수 (Probable Prime) 판별법 2. 페르마의 작은 정리 (Fermat's Little Theorem)
페르마의 마지막 정리로 유명한 수학자 페르마가 제안한 정리. p 가 a로 나눠지지 않는 소수라면 ap-1 = 1 (mod p) 를 만족해야 한다. 이때 상기 정리를 만족하지 않는 수는 합성수이며, 상기 정리를 만족하는 수는 소수일 가능성이 매우 높은 수(Probable Prime) 가 된다. 증명은 상기 링크 참조. 3. 밀러-라빈 소수판별법 (Miller-Rabin Primality Test) n 이 소수인지를 검사하려 할 때, 다음 두 식이 성립한다면 a 는 n 이 합성수라는 강한 증거가 된다. ( 이 식이 성립한다면 n 은 소수일 가능성이 매우 높은 수가 된다.)
'Contest > Algorithm' 카테고리의 다른 글
|



for all



