구간트리(Segment Tree)


구간트리는 특정 구간에서 특정한 값을 뽑아올때 유용하게 사용됩니다. 세그먼트트리라고도 하지요. 한 가지 예를 들어보도록 할게요. 어떤 배열이 아래와 같이 있다고 치구요.


int arr[] = {7, 4, 5, 1, 9, 5, 2, 11, 10};


0번 요소부터 5번째 요소까지의 가장 작은 값은 얼마인가요? 1이네요. 그쵸?

그렇다면 2번 요소부터 7번째 요소까지 가장 큰 값은 얼마일까요? 11이 됩니다. 


어떻게 찾을 수 있죠? 구간트리를 배우지 않았다면 for루프를 통해서 가장 작은 값이나 가장 큰 값을 구할겁니다.




for (i = from; i <= to; i++) {
	minVal = min(arr[i], minVal);
}


이렇게 말이죠. 아주 쉽네요!! 시간 복잡도는 O(n)입니다. 


하지만 구간에서 가장 작은 값을 계속해서 뽑아내야하는 상황이라면 구간트리를 사용해야합니다. 구간트리를 사용한다면 최소, 최대값을 찾는데 O(log n)이면 충분합니다.


구간트리의 노드는 특정 구간에서 가장 작은 값을 가지고 있습니다. 아래 트리가 구간 트리를 보여줍니다. 위의 배열을 구간트리로 표현한 모습이죠.




파란색 원 안의 숫자는 노드의 번호, 사각형 안의 숫자는 배열의 범위를 나타냅니다. 우리는 트리를 배열로 표현하기 위해서 가장 첫번째(root)는 1번 인덱스를 갖습니다. 자식 노드의 번호는 2와 3이 됩니다.

그렇다면 어떤 노드 i의 왼쪽 자식은 i*2, 오른쪽 자식은 i*2+1이 되는 것이죠.


우리가 3번 요소부터 7번 요소까지 가장 작은 값을 갖는 값을 뽑아오려면 5번, 6번, 14번 노드를 통해서 구할 수 있습니다.


이제 본격적으로 구현해보도록 합시다. 



구현(C++)


구간 트리에서 특정 구간에서 최소값을 찾는 것을 구간 최소 트리(Range Minimum Query, RMQ) 라고 합니다. 그래서 이 구조체를 만드는 것에서부터 시작합니다.




struct RMQ {
	int size;
	vector<int> minValues;
	RMQ(int *arr,int arrSize) {
		size = arrSize;
		minValues.resize(size * 4);
		init(arr, 0, size - 1,1);
	}
}

size는 배열의 size를 의미합니다. minValues는 해당 노드에서 가장 작은 값을 저장하는 벡터입니다.


왜 minValues의 사이즈를 배열의 사이즈 * 4를 할까요? 위의 트리를 다시 보게 되면 배열의 크기보다 많은 노드를 볼 수 있습니다. 완전 이진 트리를 아신다면 마지막 leaf의 개수 * 2가 트리의 노드수를 의미한다는 것을 알겁니다.

하지만 귀찮으니 4를 곱하면 된다고 하네요.


이제 이 구조체를 초기화하는 함수 init을 불러서 구간트리의 모양을 잡아보도록 합시다.


init

잘 생각해보면 간단합니다. 왼쪽 자식, 오른쪽 자식의 값을 비교해서 가장 작은 값이 지금 이 노드의 값이 됩니다.

만약 leaf노드까지 도달했다면 그 값만을 반환해주면 되죠.

그리고 구간트리의 인덱스 node라는 값도 함께 넘겨주어 현재 노드에 가장 작은 값을 저장할 수 있게끔 하면 됩니다.




int init(int *arr, int left, int right,int node) { if (left == right) return minValues[node] = arr[left]; int mid = (left + right) / 2; int leftMinValue = init(arr, left, mid, node * 2); int rightMinValue = init(arr, mid + 1, right, node * 2 + 1); return minValues[node] = min(leftMinValue, rightMinValue); }



query

이 함수는 질의, 즉 물어보는 함수입니다. 특정 구간에 가장 작은 값을 반환하여라! 라고 질문을 던져 답을 받습니다. 이 함수도 역시 잘 생각해보면 별 어려움은 없습니다. 

질의하는 범위가 노드가 커버할 수 있는 범위를 완전히 포함한다면 그 값을 내주면 됩니다.


그것이 아니라면 아주 큰 값을 리턴하면 되지요.


만약 위의 배열에서 3-7 구간에 대해 질의를 한다면 5번, 6번, 14번 노드가 3-7구간에 완전히 포함되므로 그 세개의 노드만이 자신의 값을 반환합니다. 그 후 가장 작은 값이 답이 되겠죠?


헷갈릴 수 있습니다. 노드가 커버하는 범위가 질의하는 범위에 완전히! 속해있어야합니다. 




int query(int left, int right, int node, int nodeLeft, int nodeRight) {
	if (right < nodeLeft || nodeRight < left) return INF;
	if (left <= nodeLeft&&nodeRight <= right)
		return minValues[node];

	int mid = (nodeLeft + nodeRight) / 2;
	int leftMinValue = query(left, right, node * 2, nodeLeft, mid);
	int rightMinValue = query(left, right, node * 2 + 1, mid + 1, nodeRight);

	return min(leftMinValue, rightMinValue);
}


너무 함수가 섹시하지가 않군요. 인자가 너무 많습니다. C++에 지원되는 오버로딩을 사용하여 좀 더 간편하게 부를 수 있도록 하죠.


int query(int left, int right) {
	return query(left, right, 1, 0, size - 1);
}



update

구간 트리에서 값이 바뀌면 구간의 최소값도 바뀌어여합니다. 특정 index와 새로운 value를 받게되면 구간트리의 해당 노드의 값을 바꾸고 차례대로 값을 갱신해주어야합니다. 여기서 노드의 값이 바뀌는 순서는 해당 leaf노드부터 루트까지 올라오게 됩니다.


만약 5번 인덱스가 새로운 값으로 바뀌게 되었다면 해당하는 노드의 번호 12번노드부터 6번 노드, 3번 노드, 1번 노드가 갱신되어야 하죠.



int update(int index, int value, int node, int nodeLeft, int nodeRight) {
	if (index < nodeLeft || nodeRight < index) return minValues[node];

	if (nodeLeft == nodeRight) return minValues[node] = value;
	int mid = (nodeLeft + nodeRight) / 2;
	int leftMinValue = update(index, value, node * 2, nodeLeft, mid);
	int rightMinValue = update(index, value, node * 2 + 1, mid + 1, nodeRight);
	return minValues[node]=min(leftMinValue, rightMinValue);
}

그러니까 nodeLeft==nodeRight가 같은 경우, 즉 해당하는 leaf인 경우 그 노드의 값을 갱신합니다.index의 범위 밖이면 그냥 가지고 있는 값을 반환해주면 되고, index가 포함되어 있는 경우라면 왼쪽 자식 값, 오른쪽 자식 값을 비교해서 가장 작은 값을 갖게 해주면 됩니다.


int update(int index, int value) { return update(index, value, 1, 0, size - 1); }



깔끔하게 함수를 호출할 수 있도록 오버로딩했구요.


전체코드

#include <iostream>
#include <vector>
#include <algorithm>
#define INF 99999999
using namespace std;
struct RMQ {
	int size;
	vector<int> minValues;

	RMQ(int *arr,int arrSize) {
		size = arrSize;
		minValues.resize(size * 4);
		init(arr, 0, size - 1,1);
	}

	int init(int *arr, int left, int right,int node) {
		if (left == right) return minValues[node] = arr[left];

		int mid = (left + right) / 2;
		int leftMinValue = init(arr, left, mid, node * 2);
		int rightMinValue = init(arr, mid + 1, right, node * 2 + 1);

		return minValues[node] = min(leftMinValue, rightMinValue);
	}

	int query(int left, int right, int node, int nodeLeft, int nodeRight) {
		if (right < nodeLeft || nodeRight < left) return INF;
		if (left <= nodeLeft&&nodeRight <= right)
			return minValues[node];

		int mid = (nodeLeft + nodeRight) / 2;
		int leftMinValue = query(left, right, node * 2, nodeLeft, mid);
		int rightMinValue = query(left, right, node * 2 + 1, mid + 1, nodeRight);

		return min(leftMinValue, rightMinValue);
	}

	int query(int left, int right) {
		return query(left, right, 1, 0, size - 1);
	}

	int update(int index, int value, int node, int nodeLeft, int nodeRight) {
		if (index < nodeLeft || nodeRight < index) return minValues[node];

		if (nodeLeft == nodeRight) return minValues[node] = value;
		int mid = (nodeLeft + nodeRight) / 2;
		int leftMinValue = update(index, value, node * 2, nodeLeft, mid);
		int rightMinValue = update(index, value, node * 2 + 1, mid + 1, nodeRight);
		return minValues[node]=min(leftMinValue, rightMinValue);
	}

	int update(int index, int value) {
		return update(index, value, 1, 0, size - 1);
	}
};

int main() {

	int arr[] = { 7, 4, 5, 1, 9, 5, 2, 11, 10 };
	RMQ rmq(arr, sizeof(arr) / sizeof(int));

	printf("query(0-8) : %d\n", rmq.query(0, 8));
	printf("query(1-6) : %d\n", rmq.query(1, 6));
	printf("query(7-8) : %d\n", rmq.query(7, 8));
	printf("query(3-7) : %d\n", rmq.query(3, 7));
	printf("query(0-2) : %d\n", rmq.query(0, 2));
	printf("query(0-2) : %d\n", rmq.query(4, 8));
	printf("update(index 4, value 0)) : %d\n", rmq.update(4,0));
	
}



-- 내용과 코드는 구종만의 알고리즘 문제해결 전략을 참고했습니다 

반응형
블로그 이미지

REAKWON

와나진짜

,

힙 정렬(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

와나진짜

,