▸C언어/알고리즘 및 자료구조

정렬알고리즘_힙 정렬 [6/8]

코데방 2019. 12. 9.
728x90

[ 힙 정렬이란? ]

  • 이진 트리에 기반한 힙 트리구조를 만들어가며 배열을 정렬시킴
  • 병합정렬의 메모리 효율성 문제를 해결하면서 O(n*logN)의 시간 복잡도를 보장
  • 결국 시간복잡도가 log지수의 증가를 가지려면 소그룹으로 분리시켜 정렬해나가야 함
  • 소그룹으로 쪼개서 정렬하는 방법에 힙 트리(이진 트리) 구조가 적용된 방식이라 볼 수 있음

[ 이진 트리란? ]

  • 하나의 노드에 자식 노드가 2개 이하인 트리 구조

[ 힙 트리란? ]

  • 이진 트리 구조에서, 노드 간의 관계에 규칙성이 적용 된 구조
  • 최대 힙 구조 : 부모 노드가 자식 노드보다 값이 큼 (가장 위 root 값이 최대값이 됨)
  • 최소 힙 구조 : 최대 힙 구조와 반대
  • 이진 트리를 최대 힙 또는 최소 힙 구조로 완성시킴 (최대값, 최소값을 빠르게 찾을 수 있음)

 

  • 힙 트리를 한번 만들면 힙트리의 특성을 이용해 효율적으로 최대값 또는 최소값을 찾을 수 있음
  • 힙트리를 한 번 만들고 이후 정렬 과정을 거침

정렬알고리즘_힙 정렬 [6/8]

 

  • 힙트리가 완성된 이후 root값을 마지막 값과 바꾼 후 다시 최대값을 찾음
  • 한 줄기의 자식노드만 확인해주면 힙트리가 계속 유지되기 때문에 매우 효율적인 구조

정렬알고리즘_힙 정렬 [6/8]

 

정렬알고리즘_힙 정렬 [6/8]

 


 

실제로 배열 안에서는 트리구조가 아닌 일반 규칙성을 가진 로직이기 때문에.. 규칙만 잘 찾아서 하나의 힙 트리만 완성시키고 나머지는 반복문으로 처리해주면 간단할 것 같습니다. 코딩이야 어떻게든 하면 되는건데 이런 구조나 로직을 만들어내는 분들이 참 존경스럽네요. 강의 코드를 보기 전 자체 코드를 한 번 짜보겠습니다. 이번 건 병합이나 퀵정렬에 비해 그리 어렵진 않아 보입니다.

 

정렬알고리즘_힙 정렬 [6/8]

실제 코드 구성도 병합, 퀵정렬에 비해 어렵진 않습니다. 재귀함수에 익숙해져서 그런지 뻑하면 재귀함수 호출을 쓰고 있네요. 처음엔 이런걸 어디다 쓰나 했는데.. 같은 로직을 연결된 곳에서 반복 수행할 때는 재귀함수가 참 편리한 것 같습니다.

#include <stdio.h>
void heap_arrange(int* arr, int size);
void node_swap(int* arr, int i);
void swap(int* arr, int x, int y);

void main()
{
	int arr[] = { 9, 8, 2, 10, 15, 12, 3, 22, 0, 3, 1, 1 };
	int size = sizeof(arr) / sizeof(int);

	/* 배열 출력 */
	printf("\n 정렬 전 : ");
	for (int i = 0; i < size; i++)
		printf("%d ", arr[i]);

	/* 정렬 */
	heap_arrange(arr, size);

	/* 배열 출력 */
	printf("\n 정렬 후 : ");
	for (int i = 0; i < size; i++)
		printf("%d ", arr[i]);

	printf("\n");
}

void heap_arrange(int* arr, int size)
{
	/* 최초 힙트리 생성 (자식노드 기준, 부모노드보다 작아야함) */
	for (int i = 1; i < size; i++)
		node_swap(arr, i);
	swap(arr, 0, size - 1);	// 처음과 마지막을 바꿔줌

	/* 정렬 수행 (자식노드가 있는 부모노드만 보면 됨) */
	for (int size_t = size - 1; size_t > 1; size_t--)
	{
		/* 자식노드가 있는 부모노드만 확인 */
		for (int i = 0, child = i * 2 + 1; i < size_t / 2; i++)
		{
			/* 자식 노드가 두 개 있으면 비교해서 큰 값을 지정 */
			if (child + 1 < size_t && arr[child] < arr[child + 1])
				child++;

			/* 자식노드 > 부모노드일 경우 바꿔주고 부모노드들도 확인 */
			if (arr[i] < arr[child])
			{
				swap(arr, i, child);
				node_swap(arr, i);
			}
		}
		swap(arr, 0, size_t - 1);	// 마지막 값과 바꿔줌
	}
}

/* 부모노드 모두 확인하기 */
/* parameter : 배열, 해당 노드 위치 */
/* return : void */
void node_swap(int* arr, int i)
{
	int parent_node = (i - 1) / 2;	// 부모 노드 주소

	/* root에 도달하거나 부모노드보다 작으면 리턴 */
	if (i == 0 || arr[i] < arr[parent_node])
		return;

	/* 부모노드 < 자식노드이면 바꿔주고 반복 수행 */
	if (arr[i] > arr[parent_node])
	{
		swap(arr, i, parent_node);
		node_swap(arr, parent_node);
	}
}

/* swap */
/* parameter : 배열, 바꿀 위치 두 개*/
/* return : void*/
void swap(int* arr, int x, int y)
{
	int temp = arr[x];
	arr[x] = arr[y];
	arr[y] = temp;
}

 

그럼 강의 코드를 확인하고 와볼게요.

유투브 '동빈나'님의 알고리즘 강의를 참고하였습니다.

 


 

강의에서는 do ~ while 이라는 처음보는 형식의 반복문을 사용하네요. 사실 부모노드를 찾는 일은 일정한 규칙으로 주소가 움직이기 때문에 재귀함수가 아닌 일반 반복문으로도 처리할 수 있습니다. 재귀함수는 함수 실행 시 나오는 결과를 다시 함수의 arguments로 줘서 실행시킬 때 (argument가 불규칙하게 나올 때) 가장 편리한 방식이므로 지금같은 경우는 굳이 재귀함수를 쓸 필요는 없긴 합니다. 전 연습삼아 써봤지만..

그리고 사실 이번 코드에서는 굳이 do ~ while을 쓸 필요 없이 일반 while문을 사용해도 됩니다. 무조건 먼저 한 번 수행해야하는 부분이 없어서요. 그냥 한 번 보여주실려고 써보신 것 같습니다. 혹시 몰라 일반 반복문으로 바꿔서 수행해봐도 같은 결과가 나오는 걸 확인했습니다.

추가로 최초 힙 구성을 할 때 자식노드가 부모노드보다 크지 않다면 그 부모노드의 부모노드를 검사하는 반복문은 break를 걸어주는게 좋습니다. 또한 정렬 시에도 자식노드가 없는 노드는 굳이 확인할 필요가 없으므로 그 부분에 대한 처리도 추가해주는게 좋습니다. 강의를 쉽게 하시기 위해 심플하게 작성하신 것 같아요.

 

※ Point (Do ~ while 구문)

- while의 조건을 충족할 때 Do 구문 안에 있는 내용을 수행

- Do { 수행내용 } while ( 조건 );

- 일반적인 while문과 비슷하나, 조건에 관계없이 무조건 {수행내용}을 한번 실행하고 반복문을 수행

- 그냥 while문은 조건을 충족하지 못하면 {수행내용}을 한번도 수행하지 않음

 

#include <stdio.h>
void swap(int* arr, int x, int y);
void heap_arrange(int* heap, int number);
void main()
{
	int heap[] = {9, 8, 2, 10, 15, 12, 3, 22, 0, 3, 1, 1};
	int number = sizeof(heap) / sizeof(int);
	
	/* 배열 출력 */
	printf("\n 정렬 전 : ");
	for (int i = 0; i < number; i++)
		printf("%d ", heap[i]);
	
	/* 정렬 */
	heap_arrange(heap, number);

		
	/* 배열 출력 */
	printf("\n 정렬 후 : ");
	for (int i = 0; i < number; i++)
		printf("%d ", heap[i]);
	
	printf("\n");
}

void heap_arrange(int* heap, int number)
{
	/* 최초 힙 구성 */
	for (int i = 1; i < number; i++)
	{
		int c = i;
		do
		{
			int root = (c - 1) / 2;
			if (heap[root] < heap[c])
				swap(heap, root, c);
			c = root;
		} while (c != 0);
    // heap[root] > heap[c] 일 경우 break 로직을 추가해주는 것이 좋음
	}

	/* 크기를 줄여가며 반복적으로 힙 구성 */
	for (int i = number - 1; i >= 0; i--)  
    // i > 1로 수정하고 위에 힙트리-정렬 사이에 swap을 한번 해주는게 좋을 듯
	{
		swap(heap, 0, i);  // 처음과 끝 교체
		int root = 0;
		int c = 1;
		do
		{
			c = 2 * root + 1;  // 자식노드
			if (c < i - 1 && heap[c] < heap[c + 1]) // 두번 째가 더 크면 c++
				c++;
			if (c < i && heap[root] < heap[c])
				swap(heap, root, c);
			root = c;

		} while (c < i);  // c < i/2 로 수정해주는 것이 좋음
	}
}


/* swap */
/* parameter : 배열, 바꿀 위치 두 개*/
/* return : void*/
void swap(int* arr, int x, int y)
{
	int temp = arr[x];
	arr[x] = arr[y];
	arr[y] = temp;
}
728x90

댓글