힙 정렬(Heap Sort)


정렬 시리즈 중 고급 정렬 알고리즘에 속하는 힙정렬은 힙이라는 자료구조를 토대로 정렬합니다. 그러니까 힙에 대해서 알고 와야겠죠??


힙에 대해서 모르는 분들은 아래 url를 참고하시기 바랄게요.

https://reakwon.tistory.com/42


힙에 대해서 아셨다면 이제 어떻게 정렬을 하는지 알아보도록 하지요.


정렬


위의 url의 코드를 통해서 힙정렬을 한다고 하면 의외로 쉽습니다. 메인 함수를 이렇게 바꾸어서 실행해보세요. 정렬이 아주아주 잘됩니다.


int main() {
	heap hp;
	initHeap(&hp);
	int i;
	int arr[] = { 5,3,4,1,6,10 };
	int size = sizeof(arr) / sizeof(int);
	for (i = 0; i < size; i++)
		insert(&hp, arr[i]);

	for (i = 0; i < size; i++)
		arr[i] = deleteData(&hp);

	for (i = 0; i < size; i++)
		printf("%d ", arr[i]);
	printf("\n");
}


근데 문제점이 있죠. arr이라는 배열 공간과 heap이라는 배열 공간 두개를 쓰니까 메모리의 낭비가 있습니다.


그리고 insert함수를 부르는 시간 nlog(n), deleteData함수를 부르는 시간 nlog(n)입니다.


우리는 더 잘할 수 있다

이것도 훌륭하지만 우리는 더 나아갈 수 있습니다. 더 잘할 수 있습니다.


아래의 그림은 방금 위의 배열을 단지 트리 형태로 표현한 것입니다.




최대힙을 구현할 것인데, 힙의 조건을 만족하지 않지요. 왜냐면 5가 가장 큰 원소가 아니거든요. 


아차, heap을 구현할때는 1부터 인덱스가 시작되는데 힙정렬할땐 0부터 시작한답니다. 왜냐면 공간을 아끼기 위해 그 배열 자체를 정렬할 것이기 때문이에요. 배열의 인덱스는 0부터 시작하는 것이니까요.


heapify

우리는 이것을 힙의 모양으로 고치는 것, 그 함수를 heapify라고 정의해보겠습니다.


heapify(int arr[], int here, int size) = 크기가 size인 배열 arr을 here부터 힙의 모양대로 만드는 함수


코드와 함께 heapify가 어떻게 동작하는 지 보도록 합시다.



void heapify(int arr[], int here, int size) {
	int left = here * 2 + 1;
	int right = here * 2 + 2;
	int max=here;
	if (left<size&&arr[left]>arr[max])
		max = left;
	if (right < size&&arr[right]>arr[max])
		max = right;

	if (max != here) {
		swap(&arr[here], &arr[max]);
		heapify(arr, max, size);
	}
}


이 함수 하나로 힙의 모양을 만들수는 없습니다. 

아래 왼쪽의 트리에서 우리가 heapify(arr, 0, size)를 호출한다고 치면 오른쪽의 트리로 만들어지게 됩니다.




가장 작은 1이 맨 끝에 이동함을 알 수 있죠. 하지만 최대힙은 아닙니다. 4가 가장 큰 값이 아니잖아요. 


우리는 이렇게 생각해볼 수 있습니다. 만약 자식을 갖고 있는 노드가 있다면 그 위치에서부터 heapify를  역순으로 호출하는 방식이죠. 그러니까 자식노드를 갖고 있는 노드는 0, 1, 2의 인덱스인데, heapify(arr, 2, size), heapify(arr, 1, size), heapify(arr, 0, size) 이렇게 호출하면 되지 않을까요?




그런 함수가 buildHeap이라는 함수입니다.


buildHeap


우선 우리는 자식을 갖고 있는 노드가 어떤 인덱스를 갖고 있어야하는지 알아야합니다. 어떻게 알 수 있을까요??


가장 마지막 노드는 size-1의 노드인것을 알 수 있습니다. 하지만 우리는 그 부모님 성함을 알 수 있죠. 바로 size/2 -1이 그 부모노드의 인덱스라는 것을 알 수 있습니다.

그러니까 맨 마지막의 인덱스는 5, 그 부모노드는 5/2 -1로 2가 됩니다. 


이제부터 우리는 

{ 5,3,4,1,6,10 }

이 배열을 heap으로 만드는 과정을 살펴봅니다.


초기는 아래와 같죠.




자식을 갖고 있는 노드를 알았으니 heapify(arr, 2, size)를 호출해봅시다. 그러면 위의 트리는 이렇게 바뀝니다.





10이 더 크니까 4와 자리를 바꾸죠.


이제 heapify(arr, 1, size)를 호출하면 다음과 같이 바뀝니다.




6이 더 크니까 3과 자리를 바꿉니다.


이제 heapify(arr,  0, size)를 호출합시다.



5의 자식 중 10이 5보다 크네요. 자리 바꿔줍니다.


루트까지 호출이 되었으니까 heapify는 종료됩니다.


어때요? 최대힙 모양이 완성되었죠? 

이것을 for루프로 돌리면 아래와 같은 코드가 됩니다.


void buildHeap(int arr[], int size) {
	int i,j;
	for (i = size / 2 - 1; i >= 0; i--) {
		heapify(arr, i, size);
		for (j = 0; j < size; j++)
			printf("%d ", arr[j]);
		printf("\n\n");
	}
}




이제 정렬 좀 하자..

여기까지 됐으면 거의 끝났습니다. 루트원소를 맨끝에 옮기고 다시 heapify를 호출하면 됩니다. 단, 트리의 사이즈는 하나씩 줄어듭니다.


이제 정렬과정만 보이면 되겠군요.


void heapSort(int arr[],int size) {
	int treeSize;
	buildHeap(arr, size);
	for (treeSize = size - 1; treeSize >= 0; treeSize--) {
		swap(&arr[0], &arr[treeSize]);
		heapify(arr, 0,treeSize);
	}
}

트리의 크기가 처음에는 6입니다. 크기를 점점 줄여나가는 것을 보세요. 

아래의 그림들이 위의 코드가 어떻게 동작하는지 보여줍니다.




우선 0번 노드와 5번 노드의 값을 바꾸어줍니다. 똥색 표시는 이미 정렬된 부분을 의

미합니다. 그 후 heapify를 호출하면 다시 힙의 모양을 유지하게 됩니다. 




이제는 0번 노드와 4번 노드를 교환합니다. 그리고 똥색으로 표시해주고, 이제는 트리 사이즈가 4가 됩니다. 그 후 heapify를 호출해서 사이즈 4까지 힙의 모양으로 만들어 줍니다. 5가 제일 큰 노드가 되겠네요.



이제 0번 노드와 3번 노드를 바꿉니다. 5, 6, 10까지 정렬된 것이 보이죠?? 그 후 heapify를 호출해서 최대힙의 모양으로 유지합니다.




이제 거의 다 왔습니다. 0번 노드가 가장 크므로 마지막 노드와 교체합니다. 4까지 정렬된 것을 확인할 수 있습니다. 그 후 heapify 호출해서 heap으로 유지합니다.



이제 가장 큰 노드 0번과 1번을 바꿉니다. 3까지 정렬됐습니다. 그 후 heapify호출하여 heap의 모양으로 만들어 줍니다.





이제 하나남았네요. 가장 큰 노드가 자기 자신이니까 바꾸나 마나고 heapify를 호출해도 그 모양 그대로 유지합니다.

트리를 보세요! 정렬이 된것을 확인할 수 있지요??




여기 그 전체코드가 있습니다.



#include <stdio.h>

void swap(int *a, int *b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}
void heapify(int arr[], int here, int size) {
	int left = here * 2 + 1;
	int right = here * 2 + 2;
	int max=here;
	if (left < size&&arr[left]>arr[max])
		max = left;
	if (right < size&&arr[right]>arr[max])
		max = right;

	if (max != here) {
		swap(&arr[here], &arr[max]);
		heapify(arr, max, size);
	}
}

void buildHeap(int arr[], int size) {
	int i,j;
	for (i = size / 2 - 1; i >= 0; i--) {
		heapify(arr, i, size);
		for (j = 0; j < size; j++)
			printf("%d ", arr[j]);
		printf("\n\n");
	}
}

void heapSort(int arr[],int size) {
	int treeSize;
	buildHeap(arr, size);
	for (treeSize = size - 1; treeSize >= 0; treeSize--) {
		swap(&arr[0], &arr[treeSize]);
		heapify(arr, 0,treeSize);
	}
}
void printArray(int arr[], int size) {
	int i;
	for (i = 0; i < size; i++)
		printf("%d ", arr[i]);
	printf("\n");
}
int main() {
	int arr[] = { 5,3,4,1,6,10 };
	int size = sizeof(arr) / sizeof(int);
	
	heapSort(arr, size);
	printArray(arr, size);
}


시간복잡도

힙 정렬에서 시간 복잡도는 O(nlogn)입니다. 완전 이진 트리이기때문에 높이가 균등합니다. 그래서 heapify는 logn으로 보장할 수 있습니다. 그리고 heapify를 n번 호출하기 때문에 nlogn이라는 것을 알 수 있지요. 


도움이 되셨나요??

반응형
블로그 이미지

REAKWON

와나진짜

,

힙(Heap) 개념


힙이라는 자료구조는 무엇일까요? 

힙은 일종의 트리로 수의 집합에서 가장 작은 수(키)나 가장 큰 수만을 자주 꺼내올때 유용한 자료구조입니다. 


우리는 어떤 정수형 배열이 있다고 가정해볼게요.


{ 9, 8, 7, 6, 5, 4, 3, 2, 1}


이 배열에서 가장 작은 원소를 구하려면 어떻게 해야할까요? 우선 for루프를 돌겠죠? 배열 하나하나마다 비교해서 가장 작은 값을 찾아오면 시간복잡도는 O(n)이 될겁니다. 


하지만 우리의 힙은 이 작업은 logn의 속도로 해준답니다. 

어떻게 이것이 가능할까요?




힙은 트리를 쓴다고 했죠? 이 트리는 평범한 트리는 아니고 완전 이진 트리라는 트리인데요. 항상 자식은 두개밖에 없고, leaf의 가장 왼쪽부터 채우게 됩니다.


         


그림에서 왼쪽은 완전 이진 트리인데, 오른쪽은 완전 이진 트리가 아니군요. 왼쪽 트리는 잎 노드의 왼쪽부터 전부 채워져있는 형태인데, 오른쪽 트리는 그렇지 않으니까요.


힙은 크게 두가지가 있습니다. 최소힙과 최대힙이 그것들인데요. 

최소힙은 가장 작은 값이 루트이고, 최대힙은 가장 큰 값이 루트입니다. 


우리는 그 중에서도 가장 작은 것이 루트인 최소힙을 구현해보도록 할게요. 어차피 최대힙의 구현도 거의 똑같거든요. 



최소힙


그림을 보면서 최소힙을 이해하도록 합시다. 






Ο 루트는 가장 작은 값이어야합니다. 1이 가장 작으니까 루트가 되었죠?


Ο 자식은 자신보다 크기만 하면 됩니다. 그러니까 왼쪽, 오른쪽 따질 필요없이 그냥 자식으로 넣기만 하면 된다는 거지요.


Ο 완전이진트리의 규칙을 그대로 적용해야합니다. 그림도 완전이진트리 맞죠? 이 다음 추가될 노드가 8이라면 4의 자식으로 삽입되어야합니다.


Ο 아주 중요한 개념이 있는데요. 우리는 이 힙을 배열의 형태로 구현하는 것입니다. 지금부터는 위의 노드에 써있는 숫자를 배열의 인덱스라고 보세요. 

맨 위의 루트는 1번 인덱스를 가지고 있고, 그 자식의 인덱스는 어떻게 알 수 있을까요?


위의 트리를 보면 짐작이 가지 않나요?? 왼쪽 자식은 자신의 인덱스*2오른쪽 자신은 자신의 인덱스*2 + 1이라는 사실을요. 


그러니 어떤 노드의 배열 인덱스를 parent라고 할때 자식을 구하는 식은 다음과 같습니다.


leftChild = parent*2

rightChild = parent*2+1



반대로 부모는 어떻게 알 수 있을까요? 자신의 인덱스 / 2로 부모의 인덱스를 구할 수 있습니다.

그러니까 어떤 노드를 child라고 할때, 부모의 인덱스는 다음과 같은 식으로 구할 수 있는 거죠.


parent = child/2




규칙을 알았다면 이제 본격적으로 구현해볼까요? 우선 heap이라는 구조체부터 정의해볼까요?

typedef struct heap {
	int arr[MAX_N];
	int size;
} heap;


아까 말한대로 heap을 배열의 형태로 구현할 것이기 때문에 arr이라는 배열이 구조체에 선언되어 있죠. 그리고 heap의 사이즈 역시 선언되어있습니다.


1. insert


이제 heap의 삽입과정을 보도록 합시다. 어떤 수가 추가되려면 일단 잎노드부터 계속 비교하여 자리를 찾아야되는데요, 우리는 최소힙을 구현하니까 지금 삽입될 노드가 트리의 노드보다 크면 멈춰야합니다. 하지만 루트까지 비교했는데 자신보다 작은 노드를 찾을 수 없다면 자신이 가장 작은 값이라는 것이니 루트가 되겠죠? 




아래 그림은 트리에서 1이 삽입되는 과정을 보여줍니다.




우선 맨 아래의 한자리, 즉 here가 현재 삽입될 삽입될 자리를 의미합니다. here의 인덱스는 8이 되겠네요. 8의 부모인덱스는 8/2=4 입니다. 그러니까 5와 1을 비교합니다. 1이 더 작네요. 그렇다면 5는 here의 자리로 오게됩니다.




이제 here자리는 인덱스 4가 되었습니다. 부모 인덱스는 2로써 4라는 값을 갖고 있네요. 4와 1을 비교합니다. 1이 더 작네요. 그러면 here의 자리는 부모인덱스 자리인 2가 됩니다.




부모 인덱스는 1이 됩니다. 그 값은 2로 현재 추가될 1보다 크네요. here는 1의 인덱스 위치로 옮겨집니다. 





이제 부모는 없습니다. 그러므로 here의 자리에 1이 삽입되는 것이죠.






이 상황을 구현한 코드가 아래에 있습니다. 


void insert(heap* hp,int data) {
	int here = ++hp->size;
	
	while ((here!=1)&&(data<hp->arr[here/2])) {
		hp->arr[here] = hp->arr[here / 2];
		here /= 2;
	}
	hp->arr[here] = data;
}



의외로 코드가 길지 않죠?? 


hp는 heap 구조체 포인터를 의미하는 것이고 data는 현재 추가될 데이터를 말합니다. 그래서 data는 트리의 노드 값과 비교하여 data가 더 작다면 계속 올라갑니다.

언제까지요? 자신보다 작은 트리의 노드를 만날때까지요.

하지만 더이상 트리에서 data보다 작은 값을 만날 수 없다면 here는 1이 되므로 루트자리가 됩니다.




2. deleteData


이제 heap에서 꺼내오는 함수를 구현해보도록 하죠. 우선 root 위치에 있는 값을 꺼냅니다. 그 후 가장 끝에 있는 노드를 root 위치에 다시 붙이죠. 그리고 나서 계속 자식들과 비교하여 위치를 찾습니다. 


위의 heap에서 1을 꺼내오는 과정을 아래에서 보여주고 있습니다. 







1을 우선 꺼냅니다. 1은 나가있어 뒤지기 시르면 

루트가 비어있네요. 루트를 가장 끝에 있는 노드 5로 매꿉니다.




이제 5라는 노드의 자식을 비교해서 위치를 찾는데요. 두 자식보다 자신이 더 작다면 멈추면 됩니다. 두 자식보다 가장 작은 녀석은 2인데, 2가 5보다 작으므로 5와 2의 위치를 바꿔줍니다. 


그러면 아래와 같은 트리가 되겠죠?






5의 자식은 4와 7이 되었습니다. 둘 중 가장 작은 자식은 4인데, 5는 4보다 크지요? 그러니까 4와 5의 위치를 바꿔줍니다. 




이제 더 이상 내려갈 곳이 없군요. 5의 위치는 이제 정해졌습니다.

자 보세요. 1을 꺼냈어도 완전 이진트리의 모양을 유지하고 있고 가장 작은 값이 루트인것이 보이죠? 이제 코드로 구현하면 됩니다.



int deleteData(heap *hp) {
	if (hp->size == 0) return -1;
	int ret = hp->arr[1];
	hp->arr[1]=hp->arr[hp->size--];
	int parent = 1;
	int child;

	while (1) {
		child = parent * 2;
		if (child + 1 <= hp->size && hp->arr[child]>hp->arr[child + 1])
			child++;

		if (child>hp->size||hp->arr[child] > hp->arr[parent]) break;
		
		swap(&hp->arr[parent], &hp->arr[child]);
		parent = child;
	}
	
	return ret;
	
}


우선 heap의 사이즈가 0이면 -1을 리턴하도록 했습니다.
그리고 root의 요소를 반환할 변수에 저장하고 난 후 새로운 루트를 마지막 노드로 바꾸는게 보이세요? 이 구간이요.

int ret = hp->arr[1];
	hp->arr[1]=hp->arr[hp->size--];

이후 while루프 안에서 가장 작은 자식과 자신을 비교해서 자신이 더 작을때까지 돌거나 더 이상 자식이 없을 때까지 돕니다.




전체 코드


이제 전체코드를 통해서 직접 실행해 보세요.


#include <stdio.h>
#define MAX_N 1024
typedef struct heap {
	int arr[MAX_N];
	int size;
} heap;

void swap(int *a, int *b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}
void initHeap(heap *hp) {
	hp->size = 0;
}
void insert(heap* hp,int data) {
	int here = ++hp->size;
	
	while ((here!=1)&&(data<hp->arr[here/2])) {
		hp->arr[here] = hp->arr[here / 2];
		here /= 2;
	}
	hp->arr[here] = data;
}

int deleteData(heap *hp) {
	if (hp->size == 0) return -1;
	int ret = hp->arr[1];
	hp->arr[1]=hp->arr[hp->size--];
	int parent = 1;
	int child;

	while (1) {
		child = parent * 2;
		if (child + 1 <= hp->size && hp->arr[child]>hp->arr[child + 1])
			child++;

		if (child>hp->size||hp->arr[child] > hp->arr[parent]) break;
		
		swap(&hp->arr[parent], &hp->arr[child]);
		parent = child;
	}
	
	return ret;
	
}
int main() {
	heap hp;
	initHeap(&hp);
	
	insert(&hp, 3);
	insert(&hp, 5);
	insert(&hp, 1);
	insert(&hp, 2);
	insert(&hp, 8);
	
	printf("%d\n", deleteData(&hp));
	printf("%d\n", deleteData(&hp));
	printf("%d\n", deleteData(&hp));

	printf("\n");
	insert(&hp, 9);
	insert(&hp, 3);
	printf("%d\n", deleteData(&hp));
	printf("%d\n", deleteData(&hp));

	printf("\n");
	insert(&hp, 1);
	insert(&hp, 9);
	insert(&hp, 10);
	printf("%d\n", deleteData(&hp));
	printf("%d\n", deleteData(&hp));
	printf("%d\n", deleteData(&hp));
	printf("%d\n", deleteData(&hp));
	printf("%d\n", deleteData(&hp));
	printf("%d\n", deleteData(&hp));
	printf("%d\n", deleteData(&hp));

}


제가 직접 구현했기때문에 버그가 있을 수 있습니다.
혹시나 있다면 알려주세요^^


반응형
블로그 이미지

REAKWON

와나진짜

,