우선순위 큐(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