Frinee의 코드저장소

우선순위 큐(Priority Queue)와 힙(Heap)

by Frinee
이 글은 윤성우 저 - "윤성우의 열혈 자료구조"를 공부하고 정리하여 작성하였습니다.

 

1. 우선순위 큐의 이해

1.1. 우선순위 큐와 우선순위의 이해

  • 우선순위 큐도 enqueue와 dequeue가 일반 큐와 같음
  • 하지만 우선순위 큐의 결과는 들어간 순서에 상관없이 우선순위가 높은 데이터가 먼저 나옴

1.2. 우선순위 큐의 구현 방법

  • 배열 기반으로 구하는 방법
  • 연결 리스트를 기반으로 구현하는 방법
  • 힙(heap)을 이용하는 방법

  • 배열의 경우, 데이터의 우선순위가 높을수록 배열의 앞쪽에 데이터를 위치시킴
  • 데이터 반환 및 소멸은 쉽지만 데이터를 삽입 및 삭제하는 과정에서 데이터를 계속 한 칸씩 뒤로 밀거나 당기는 연산을 수반해야 하는 문제가 생김
  • 삽입의 위치를 찾기 위해 배열에 저장된 모든 데이터와 우선순위 비교를 진행해야 할 수 있음
  • 연결 리스트의 경우, 삽입의 위치를 찾기 위해서 첫 번째 노드에서부터 시작하여 마지막 노드에 저장된 데이터와 비교해야 할 수도 있는 문제가 있음

1.3. 힙(heap)의 소개

  • 힙은 완전 이진 트리로 이루어져 있음
  • 그리고 모든 노드에 저장된 값은 자식 노드에 저장된 값보다 크거나 같아야 함.
  • 즉, 루트 노드가 가장 커야 함

 

2. 힙의 구현과 우선순위 큐의 완성

2.1. 힙에서의 데이터 저장과정

  • 새로운 데이터는 우선순위가 제일 낮다는 가정 하에 마지막 위치에 저장
  • 이후 부모 노드와 우선순위를 비교하여 위치를 바꿔야 할 경우 교체
  • 제대로 된 위치를 찾을 때까지 계속 진행

2.2. 힙에서의 데이터 삭제과정

  • 우선순위 큐에서의 삭제는 가장 높은 우선순위의 데이터 삭제를 의미
  • 마지막 노드를 루트 노드 자리로 옮긴 후 자식 노드와의 비교를 통해 자기 자리를 찾게 함.
  • 이때 루트 노드와 왼쪽 자식과 오른쪽 자식 중 우선순위가 더 높은 노드를 먼저 비교해야 함.

2.3. 삽입/삭제 성능 평가 및 힙 구현 자료구조

  • 배열
    • 데이터 저장 시간 복잡도: O(n)
    • 데이터 삭제 시간 복잡도: O(1)
  • 연결리스트
    • 데이터 저장 시간 복잡도: O(n)
    • 데이터 삭제 시간 복잡도: O(1)
    • 데이터 저장 시간 복잡도: O(logn)
    • 데이터 삭제 시간 복잡도: O(logn)
  • 의외로 힙은 배열을 기반으로 구현해야 함.
  • 연결리스트 기반으로 힙을 구현할 경우, 새로운 노드를 힙의 마지막 위치에 추가하는 것이 어려움
  • 부모 노드의 인덱스 값: n
  • 왼쪽 자식 노드의 인덱스 값: 2*n
  • 오른쪽 자식 노드의 인덱스 값: 2*n + 1

2.4. 힙의 구현

  • 숙지할 내용
    • 힙은 완전 이진 트리이다.
    • 힙의 구현은 배열을 기반으로 하며 인덱스가 0인 요소는 비워둔다.
    • 따라서 힙에 저장된 노드의 개수와 마지막 노드의 고유번호는 일치한다.
    • 노드의 고유번호가 노드가 저장되는 배열의 인덱스 값이 된다.
    • 우선순위를 나타내는 정수 값이 작을수록 높은 우선순위를 나타낸다고 가정한다
  • 헤더 파일
typedef char HData;
typedef int Priority;

typedef struct _heapElem
{
	Priority pr; // 값이 작을수록 높은 우선순위
	HData data;
} HeapElem;

typedef struct _heap
{
	int numOfData;
	HeapElem heapArr[HEAP_LEN];
} Heap;

void HeapInit(Heap * ph);
int HIsEmpty(Heap * ph);

void HInsert(Heap * ph, HData data, Priority pr);
HData HDelete(Heap * ph);

 

  • 구현 코드
void HeapInit(Heap *ph)
{
	ph->numOfData = 0;  // 힙의 초기화
}

int HIsEmpty(Heap *ph)  // 힙이 비어있는지 확인
{
	if(ph->numOfData == 0)
		return TRUE;
	else
		return FALSE;
}

int GetParentIDX(int idx)  // 부모 노드의 인덱스 값 반환
{
	return idx/2;
}

int GetLChildIDX(int idx)
{
	return idx*2;
}

int GetRChildIDX(int idx)
{
	return idx*2 + 1;
}

// 두 개의 자식 노드 중 높은 우선순위의 자식 노드 인덱스 값 반환
int GetHiPriChildIdx(Heap *ph, int idx)
{
	// 자식 노드가 존재하지 않는다면
	if(GetLChildIDX(idx) > ph->numOfData)
		return 0;
	
	// 자식 노드가 왼쪽 자식 노드 하나만 존재한다면
	else if(GetLChildIDX(idx) == ph->numOfData)
		return GetLChildIDX(idx);
	
	// 자식 노드 둘 다 존재한다면
	else
	{
		// 오른쪽 자식 노드의 우선순위가 높다면
		if(ph->heapArr[GetLChildIDX(idx)].pr > ph->heapArr[GetRChildIDX(idx)].pr)
			return GetRChildIDX(idx);   // 오른쪽 자식 노드 인덱스 값 반환
		else
			return GetLChildIDX(idx);   // 왼쪽 자식 노드의 인덱스 값 반환
	}
}

// 힙에 데이터 저장
void HInsert(Heap *ph, HData data, Priority pr)
{
// 새 노드가 우선순위가 가장 낮다는 가정하에 마지막 위치에서 시작
	int idx = ph->numOfData+1;
	HeapElem nelem = {pr, data};
	
	while(idx != 1)
	{
		if(pr < (ph->heapArr[GetParentIDX(idx)].pr);
			ph->heapArr[idx] = heapArr[GetParentIDX(idx)];
			idx = GetParentIDX(idx);
	}
	
	ph->heapArr[idx] = nelem;
	ph->numOfData += 1; 
}

// 힙에서 데이터 삭제
HData HDelete(Heap *ph)
{
	HData retData = (ph->heapArr[1]).data;  // 반환을 위해서 삭제할 데이터 저장
	HeapElem lastElem = ph->heapArr[ph->numOfData];  // 힙의 마지막 노드 저장
	
	// 아래의 변수 parentIdx에는 마지막 노드가 저장될 위치정보가 담긴다.
	int parentIdx = 1;  // 루트 노드가 위치해야 할 인덱스 값 저장
	int childIdx;
	
	// 루트 노드의 우선순위가 높은 자식 노드를 시작으로 반복문 시작
	while(childIdx == GetHiPriChildIDX(ph, parentIdx))
	{
		if(lastElem.pr <= ph->heapArr[childIdx].pr)  // 마지막 노드와 우선순위 비교
			break;  // 마지막 노드의 우선순위가 높으면 반복문 탈출
			
		ph->heapArr[parentIdx] = ph->heapArr[childIdx];  
		parentIdx = childIdx;  // 마지막 노드가 저장될 위치 정보를 한 레벨 내림
	}
	
	ph->heapArr[parentIdx] = lastElem;  // 마지막 노드 최종 저장
	ph->numOfData -= 1;
	return retData;
}
  • 변경점
    • 우선순위를 매번 정해서 알려줘야 함.
    • 우선순위의 판단 기준을 힙에 설정할 수 있어야 함
  • 수정
    • heapElem의 역할이 없어져 삭제됨
    • heap 구조에 두 개의 데이터를 대상으로 우선순위가 낮고 높음을 판단하는 함수를 등록하기 위한 포인터 변수 comp 등록

 

  • 변경점만 정리한 코드
// heapElem 삭제
//  typedef struct _heapElem
// {
//	 Priority pr; // 값이 작을수록 높은 우선순위
//	 HData data;
// } HeapElem;  

typedef char HData;
typedef int (*PriorityComp)(HData d1, HData d2);

typedef struct _heap
{
	PriorityComp *comp; // 추가
	int numOfData;
	HeapElem heapArr[HEAP_LEN];
} Heap;

void HeapInit(Heap *ph, PriorityComp pc)
{
	ph->numOfData = 0;  // 힙의 초기화
	ph->comp = pc;
}

... 나머지 동일

// 두 개의 자식 노드 중 높은 우선순위의 자식 노드 인덱스 값 반환
int GetHiPriChildIdx(Heap *ph, int idx)
{
	// 자식 노드가 존재하지 않는다면
	if(GetLChildIDX(idx) > ph->numOfData)
		return 0;
	
	// 자식 노드가 왼쪽 자식 노드 하나만 존재한다면
	else if(GetLChildIDX(idx) == ph->numOfData)
		return GetLChildIDX(idx);
	
	// 자식 노드 둘 다 존재한다면
	else
	{
		// 오른쪽 자식 노드의 우선순위가 높다면
	// if(ph->heapArr[GetLChildIDX(idx)].pr > ph->heapArr[GetRChildIDX(idx)].pr)
		if(ph->comp(ph->heapArr[GetLChildIDX(idx)], ph->heapArr[GetRChildIDX(idx)]) < 0) // 대체
			return GetRChildIDX(idx);   // 오른쪽 자식 노드 인덱스 값 반환
		else
			return GetLChildIDX(idx);   // 왼쪽 자식 노드의 인덱스 값 반환
	}
}

// 힙에 데이터 저장
void HInsert(Heap *ph, HData data, Priority pr)
{
// 새 노드가 우선순위가 가장 낮다는 가정하에 마지막 위치에서 시작
	int idx = ph->numOfData+1;
//	HeapElem nelem = {pr, data};
	
	while(idx != 1)
	{
	// if(pr < (ph->heapArr[GetParentIDX(idx)].pr);
		if(ph->comp(data, ph->heapArr[GetParentIDX(idx)]) > 0) // 대체
			ph->heapArr[idx] = heapArr[GetParentIDX(idx)];
			idx = GetParentIDX(idx);
	}
	
	ph->heapArr[idx] = nelem;
	ph->numOfData += 1; 
}

// 힙에서 데이터 삭제
HData HDelete(Heap *ph)
{
	HData retData = (ph->heapArr[1]).data;  // 반환을 위해서 삭제할 데이터 저장
	HeapElem lastElem = ph->heapArr[ph->numOfData];  // 힙의 마지막 노드 저장
	
	// 아래의 변수 parentIdx에는 마지막 노드가 저장될 위치정보가 담긴다.
	int parentIdx = 1;  // 루트 노드가 위치해야 할 인덱스 값 저장
	int childIdx;
	
	// 루트 노드의 우선순위가 높은 자식 노드를 시작으로 반복문 시작
	while(childIdx == GetHiPriChildIDX(ph, parentIdx))
	{
	// if(lastElem.pr <= ph->heapArr[childIdx].pr)  // 마지막 노드와 우선순위 비교
	   if(ph->comp(lastElem, ph->heapArr[childIdx]) >= 0)  // 대체
			break;  // 마지막 노드의 우선순위가 높으면 반복문 탈출
			
		ph->heapArr[parentIdx] = ph->heapArr[childIdx];  
		parentIdx = childIdx;  // 마지막 노드가 저장될 위치 정보를 한 레벨 내림
	}
	
	ph->heapArr[parentIdx] = lastElem;  // 마지막 노드 최종 저장
	ph->numOfData -= 1;
	return retData;
}

자료

  • 윤성우의 열혈 자료구조 (윤성우 저, 2023.10)

'[컴퓨터 과학자 스터디] > 자료구조' 카테고리의 다른 글

탐색(Search) 1  (1) 2024.12.02
정렬(Sorting)  (0) 2024.11.27
트리(tree)  (0) 2024.11.23
큐(queue)  (0) 2024.11.21
스택(stack)  (0) 2024.11.21

블로그의 정보

프리니의 코드저장소

Frinee

활동하기