힙 정렬의 "힙" 은 두가지 용어로 사용된다.
첫번째는 자료구조 힙이고, 두번째는 메모리공간 힙이다.
여기서는 자료구조 힙에 대해 논의한다.
힙이란 간단하게 두가지 힙이 존재한다. 최대힙과 최소힙이다.
(피보나치힙은 제외)
최대힙의 정의는 부모노드의 값은 반드시 자식노드의 값보다 크고, 완전 이진트리의 형태를 띈다.
최소힙은 그 반대의 경우이다.
여기서 힙을 실제로 노드기반 자료구조로 표현할 수 도있지만, 이는 멍청한 짓이다.
그 첫번째 근거는, 노드기반 자료구조의 가장 큰 단점인 메모리할당 함수의 수행시간이다.
당연한 이야기지만, 시스템콜인 할당함수는 매우 많은 시간이 소요된다.
둘째로 힙정렬에서 사용되는 힙은 입력값이 배열이다. 즉 결과도 배열인데, 그 중간 연산으로
노드기반 힙을 사용할 이유가 없다.
따라서 이를 배열로 표기하는데, 부모와 왼쪽자식, 오른쪽 자식을 찾는 프로시저를 다음과 같이 정의 할 수 있다.
#define HEAP_PARENT(I) (((I)-1)>>1)
#define HEAP_LEFT(I) (((I)<<1)+1)
#define HEAP_RIGHT(I) (((I)<<1)+2)
이는 인라인함수나 매크로로 작성하는것이 좀더 좋다.
우선 힙 정렬 알고리즘에서는 최대힙을 사용한다.(오름차순 정렬일 경우엔)
힙은 완전 이진트리를 기반으로 하므로, 원소의 개수가 n 개일때 높이는 O(lg n) 이다.
이제 몇가지 문제를 풀어보자.
리프 임을 보여라.
그럼 이제 , 힙을 만드는 함수를 정의하여 보자.
힙을 만드는 방식은 두가지가 있다. 상향식 방법과 하향식 방법이다.
당연히 ! 상향식 방법이 더 간단하고 속도도 빠르다. 일반적으로 힙을 구성할때는 상향식 방법을 쓰고, 힙정렬을 할땐 하향식 방법을 사용한다.
상향식 방법은 루트쪽으로 가면서 검사하는 방법이고 , 하향식 방법은 리프쪽으로 가면서 검사하는 방법이다.
//build heap down to up ( this function is use in forword loop )
#define HEAPIFY_D2U(ARRAY,I,ELEM_SZ,COMPARE) do{\
int IDX=I;\
while(IDX>0 && COMPARE(&ARRAY[IDX],&ARRAY[HEAP_PARENT(IDX)])>0){\
SWAP(ARRAY[IDX],ARRAY[HEAP_PARENT(IDX)],ELEM_SZ);\
IDX=HEAP_PARENT(IDX);\
}\
}while(0)
//build heap up to down ( this function is use in reverse loop )
#define HEAPIFY_U2D(ARRAY,INDEX,ELEM_SZ,BOUND,COMPARE) do{\
int IDX=INDEX;\
int L,R,C;\
do{\
L=HEAP_LEFT(IDX);\
R=HEAP_RIGHT(IDX);\
if(L<BOUND && COMPARE(&ARRAY[L],&ARRAY[IDX])>0)C=L;\
else C=IDX;\
if(R<BOUND && COMPARE(&ARRAY[R],&ARRAY[C])>0)C=R;\
if(C==IDX)break;\
SWAP(ARRAY[C],ARRAY[IDX],ELEM_SZ);\
IDX=C;\
}while(1);\
}while(0)
매크로 함수인데, COMPARE에 비교 함수를 넣어도 되고, 매크로 비교함수를 넣어도 좋다.
#include<stdio.h>
#include<time.h>
#include"sort/heap.h"
#define MAXN 10000000
typedef struct Z{
#define Z_COMPARE(A,B) ((A).a < (B).a)
int a; //primary key
int b;
int c;
}Z;
int main() {
Z *arr=(Z*)calloc(MAXN,sizeof(Z));
int i;
for (i = 0; i < MAXN; i++) {
arr[i].a = rand() % (MAXN * 10);
arr[i].b = rand() % (MAXN * 10);
arr[i].c = rand() % (MAXN * 10);
}
for (i = MAXN/2; i >= 0; i--)
HEAPIFY_U2D(arr, i, sizeof(Z), MAXN, Z_COMPARE);
free(arr);
return 0;
}
위는 사용 예시이다.
힙을 만드는데 만일 상향식 방법인 D2U를 사용한다면 모든 원소에대해 구성을 해야한다.
하지만 하향식 방법을 사용한다면, n/2 부터 0 까지만 돌면된다.
근데!!! 하향식 방법이 좀더 빠르다
이를 힙을 만든다. 라고 표현을 한다.
HEAPIFY는 복잡도가 lg n임을 증명할 수 있다.
그리고 이를 n/2 번 반복한다. 이를 복잡도가 n lg n 이라고 말할수도 있지만, 좀더 좋은 방법이 있다.
먼저 원소가 n개 있을때 이 힙의 높이는 lower(lg n)임을 보였다.
그리고 높이가 h인 노드수는 upper(n / 2^(h+1) ) 임을 보일 수 있다.
이를 근거로 하여 힙을 구성하는 함수의 복잡도를 다음과 같이 쓸 수 있다.
sigma h 0 to lower(lg n) (upper(n/2^(h+1)) * O(h)
이는 O(n* sigma h 0 to lower(lg n) (h/2^h) )
따라서 이 공식은 sigma k=0 to INF (kx^k)
는 x/(1-x)^2
의 공식에 따라
x에 1/2를 대입하여 풀 수 있다.
따라서 이 힙만드는 함수의 복잡도는 선형시간에 동작한다고 말할 수 있다.
이를 코드로 표현하면 아래와 같다.
#define BUILD_HEAP(ARRAY,ELEM_SZ,LENGTH,COMPARE) do{\
int i=(LENGTH)>>1;\
while(i--) {HEAPIFY_U2D(ARRAY,i,ELEM_SZ,LENGTH,COMPARE);}\
}while(0)
이제 힙소트를 구현하면 아래와 같다.
#define IMPLEMENT_HEAPSORT(TYPE)\
void ATTATCH(heapsort_,TYPE)(TYPE* base,int size,int(*COMPARE)(const TYPE*,const TYPE*)){\
int i=size;\
int hsz=size;\
BUILD_HEAP(base,sizeof(TYPE),size,COMPARE);\
while(i-- > 0){\
SWAP(base[0],base[i],sizeof(TYPE));\
--hsz;\
HEAPIFY_U2D(base,0,sizeof(TYPE),hsz,COMPARE);\
}\
}\
마치 C++의 템플릿 처럼 모든 자료형에 대한 최적화된 소스를 생성한다.
제네릭한 소트는 C에서 void* 를 사용하여 구현하는데, 많이 짜본 사람들은 이 방법의 단점을 알것이다.
제네릭한 만큼 시간이 소요되는데...
본 매크로함수의 시간은 좀 더 테스트를 해봐야겠다.
stdlib.h
의 qsort
와의 비교에서는 2배정도 느린 결과를 보여주었다.
자 이제 이걸 개선해보자.
버블정렬이 선택정렬로
gnome정렬이 삽입정렬로 개선되는 방식의 swap 없애기 기법을 통해 개선을 하면
속도는 다음과 같이 개선된다.
gcc -O3
갱신전
array make time : 0.021289
array size : 1000000
quicksort : 0.177737
heapsort : 0.348398
갱신후
array make time : 0.022693
array size : 1000000
quicksort : 0.188064
heapsort : 0.264053