2751. 수 정렬하기 2
https://www.acmicpc.net/problem/2751
문제
N개의 수가 주어졌을 때, 이를 오름차순으로 정렬하는 프로그램을 작성하시오.
입력
첫째 줄에 수의 개수 N(1<=N<=1,000,000)이 주어진다. 둘째 줄부터 N개의 줄에는 숫자가 주어진다. 이 수는 절대값이 1,000,000보다 작거나 같은 정수이다. 수는 중복되지 않는다.
출력
첫째 줄부터 N개의 줄에 오름차순으로 정렬한 결과를 한 줄에 하나씩 출력한다.
예제 입력
5
5
4
3
2
1
예제 출력
1
2
3
4
5
1. 힙 정렬 heap sort 소개
O(n * log n)의 시간복잡도를 가지는 또 다른 정렬 알고리즘을 알아보자
힙 정렬은 말 그래도 힙 자료구조를 사용한 정렬방법이다.
따라서 이해하기 위해선 힙이 무엇인지부터 알아야 한다!
1-1) 힙 자료구조 소개
우선 힙은 완전이진트리다.
이진트리라 함은 한 노드가 최대 두 개의 자식노드를 가진다는 뜻이고,
완전이진트리라 함은 마지막 level = depth 를 제외하고 모든 레벨은 노드가 가득 차있으며, 마지막 레벨은 노드가 왼쪽부터 채워져 있다는 뜻이다.
힙의 강점은 부모-자식 간에 대소관계가 전부 적용되어 있다는 것이다. 단 형제들 끼리는 적용하지 않는다.
(최대힙이라면) 따라서 루트노드는 가장 큰 노드가 될 것이다.
따라서 이러한 힙(최대 힙)을 유지한다면 O(1)에 가장 큰 수를 찾을 수 있다.
우선순위 큐도 비슷한 자료구조로 구현되어 있다고 한다.
1-2) 힙 자료구조 구성하기
힙은 트리로 되어있지만, 완전이진트리라는 점에서 배열로 구현할 수 있다.
루트노드를 인덱스 0으로 잡을시, 부모 노드의 인덱스가 x라면, 왼쪽 자식의 인덱스는 (x * 2 + 1),
오른쪽 자식의 인덱스는 (x * 2 + 2) 이다.
루트노드를 인덱스 1으로 잡을시, 부모 노드의 인덱스가 x라면, 왼쪽 자식의 인덱스는 x * 2,
오른쪽 자식의 인덱스는 (x * 2 + 1) 이다.
이제 전체 배열을 힙으로 만들어 보자!
힙으로 만드는데는 부모노드를 조지는 방법이 효과적이다.
간단히 노드 세 개의 이진트리를 생각해보면,
루트 노드가 3이고, 두 자식 노드가 모두 3보다 크다면 어찌되었건 부모 노드를 아무 자식과 스왑해주면 끝이다.
그래서 작은 부분 부분 힙을 만들어주면서 확장시키는 방법을 쓴다.
코드로 보면 이해가 빠르당
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | void down_heap(int x, int size) { int left, right, p; while (x < size) { left = x * 2 + 1; right = x * 2 + 2; if (left >= size && right >= size) break; p = x; if (left < size && arr1[p] < arr1[left]) p = left; if (right < size && arr1[p] < arr1[right]) p = right; if (p == x) break; int tmp = arr1[x]; arr1[x] = arr1[p]; arr1[p] = tmp; x = p; } } | cs |
x를 부모 노드로 잡고 힙이 될 조건(부모가 제일 큼)을 만족할 때 까지 밑으로 조져버린다.
함수를 배열 맨 뒤에서 부터 호출해주면 되는데,
시간을 절약하기 위해선 리프 노드에 대해선 호출할 필요가 없다는 점을 기억하자.
1-3) 힙 정렬하기
이제 힙을 완성했으니 정렬에 써먹어 보자.
힙의 장점 : 최대값을 O(1)에 알 수 있다 / 를 활용하는 것이다.
오름차순이라면 배열의 최대값은 배열 맨 뒤에 있어야 한다. O(1)에 알 수 있다. 힙에서 베껴온다.
그럼 그 다음으로 큰 수는 어떻게 알 수 있을까?
일단 다음으로 큰 수는 루트(부모)의 두 자식 중에 한 놈이다.
하지만 우리의 목표는 계속 힙 구조를 유지하면서 장점을 써먹는 것이므로, 루트를 잃어버린 힙을 재구성해야 한다.
재구성은 간단하다. 위에서 했던 down_heap()을 활용한다.
루트를 힙의 누군가로 새롭게 대체하고 밑으로 조져버리면, 루트의 두 자식 중에 큰 놈이 위로 올라올 것이고, 대체자는 본연의 자리를 찾아갈 것이므로 힙이 새롭게 재구성되는 것이다.
루트를 새로 채울 놈은 누구를 써야 하는가? 만만한건 제일 마지막에 있는 리프 노드다.
그림으로 확인해보자.
2. 코드
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | #include <stdio.h> #include <vector> using namespace std; int n; vector<int> arr1, arr2; void down_heap(int x, int size) { int left, right, p; while (x < size) { left = x * 2 + 1; right = x * 2 + 2; if (left >= size && right >= size) break; p = x; if (left < size && arr1[p] < arr1[left]) p = left; if (right < size && arr1[p] < arr1[right]) p = right; if (p == x) break; int tmp = arr1[x]; arr1[x] = arr1[p]; arr1[p] = tmp; x = p; } } int main() { scanf("%d", &n); arr1.resize(n); arr2.resize(n); for (int i = 0; i < n; ++i) scanf("%d", &arr1[i]); for (int i = (n - 1) / 2; i >= 0; --i) down_heap(i, n); for (int i = n - 1; i >= 0; --i) { arr2[i] = arr1[0]; arr1[0] = arr1[i]; down_heap(0, i); } for (auto a : arr2) printf("%d\n", a); return 0; } | cs |
3. 시간복잡도
힙 정렬 알고리즘은 간단히 쓰면 다음의 단계를 거친다.
1) 주어진 배열로 힙을 만든다.
2) 루트노드를 새로운 배열로 옮기고, 힙의 말단으로 루트를 대체한뒤, 다운힙 시킨다.
1번의 과정은 얼마나 걸릴까?
우선 힙을 만드는데 down_heap을 썼던 점을 기억하자.
down_heap은 결국엔 이진트리의 높이만큼 수행될 것이다 -> O(log n)
또한 배열의 맨 뒤 부터 밑으로 조져버린다는 점에 의해 -> O(n * log n) 안에 수행된다.
2번 과정도 동일하다. O(1)에 루트노드에 접근할 수 있으며, 다운 힙은 O(log n) 안에 수행된다. 이를 n개에 대해 연산한다.
따라서 O(n * log n) 임을 알 수 있다.
'알고리즘 > 정렬 알고리즘' 카테고리의 다른 글
5. 카운팅 정렬 counting sort (0) | 2017.09.27 |
---|---|
3. 병합 정렬 merge sort (0) | 2017.09.25 |
2. 거품 정렬 bubble sort (0) | 2017.09.25 |
1. 삽입 정렬 insertion sort (0) | 2017.09.25 |