+특정한 조건을 만족하는 이진트리(binary tree)를 기반으로 구현된다.
Max Heap: 부모 노드의 값이 자식 노드의 값보다 크거나 같은 완전이진트리이다.
Min Heap: 부모 노드의 값이 자식 노드의 값보다 작거나 같은 완전이진트리이다.
배열을 이용한 구현: 완전이진트리를 배열에 저장하여 구현한다.
연결 리스트를 이용한 구현: 각 노드가 자식 노드를 가리키는 포인터를 가지는 구조이다.
Heap에 새로운 값을 추가한다.
일반적으로는 트리의 맨 끝 노드에 새로운 값을 추가하고, 부모 노드와 비교하여 Heap 조건을 만족시키는 과정을 거친다.
Heap에서 가장 큰 또는 가장 작은 값을 제거한다.
일반적으로는 Root 노드를 삭제하고, Heap 조건을 만족시키는 과정을 거친다.
삽입의 시간 복잡도: O(log n)
삭제의 시간 복잡도: O(log n)
이진트리에서 깊이는 log n을 초과할 수 없으므로, Heap의 삽입과 삭제는 최악의 경우에도 O(log n)의 시간 복잡도를 가지낟.
Heap의 단점: Heap에서 원하는 값을 탐색하는 것은 비효율적이다.
Heap의 보완 방법:
이진 탐색(binary search)을 이용한 자료구조를 추가로 사용하여 보완
Heap 내에서 빠르게 탐색이 가능하도록 구현한다.
알고리즘 수업 - 힙 정렬 2(백준 : 24174번)
오늘도 서준이는 최소 힙 기반 힙 정렬 수업 조교를 하고 있다. 아빠가 수업한 내용을 학생들이 잘 이해했는지 문제를 통해서 확인해보자.
N개의 서로 다른 양의 정수가 저장된 배열 A가 있다. 힙 정렬로 배열 A를 오름차순 정렬할 경우 배열 A의 원소가 K 번 교환된 직후의 배열 A를 출력해 보자.
크기가 N인 배열에 대한 힙 정렬 의사 코드는 다음과 같다.
heap_sort(A[1..n]) { # A[1..n]을 정렬한다.
build_min_heap(A, n);
for i <- n downto 2 {
A[1] <-> A[i]; # 원소 교환
heapify(A, 1, i - 1);
}
}
build_min_heap(A[], n) {
for i <- ⌊n / 2⌋ downto 1
heapify(A, i, n)
}
# A[k]를 루트로 하는 트리를 최소 힙 성질을 만족하도록 수정한다.
# A[k]의 두 자식을 루트로 하는 서브 트리는 최소 힙 성질을 만족하고 있다.
# n은 배열 A의 전체 크기이며 최대 인덱스를 나타낸다.
heapify(A[], k, n) {
left <- 2k; right <- 2k + 1;
if (right ≤ n) then {
if (A[left] < A[right]) then smaller <- left;
else smaller <- right;
}
else if (left ≤ n) then smaller <- left;
else return;
# 최소 힙 성질을 만족하지 못하는 경우 재귀적으로 수정한다.
if (A[smaller] < A[k]) then {
A[k] <-> A[smaller];
heapify(A, smaller, n);
}
}
입력
첫째 줄에 배열 A의 크기 N(5 ≤ N ≤ 500,000), 교환 횟수 K(1 ≤ K ≤ 108)가 주어진다.
다음 줄에 서로 다른 배열 A의 원소 A1, A2, ..., AN이 주어진다. (1 ≤ Ai ≤ 109)
출력
배열 A에 K 번 교환이 발생한 직후의 배열 A를 한 줄에 출력한다. 교환 횟수가 K 보다 작으면 -1을 출력한다.
예제 입력 1
5 2
2 5 1 4 3
예제 출력 1
1 3 2 4 5
과정설명 :
2 5 1 4 3(heapify(A, 2, 5)) -> 2 3 1 4 5(heapify(A, 1, 5)) -> 1 3 2 4 5(A[1] <-> A[5]) -> 5 3 2 4 1(heapify(A, 1, 4)) -> 2 3 5 4 1(A[1] <-> A[4]) -> 4 3 5 2 1(heapify(A, 1, 3)) -> 3 4 5 2 1(A[1] <-> A[3]) -> 5 4 3 2 1(heapify(A, 1, 2)) -> 4 5 3 2 1(A[1] <-> A[2]) -> 5 4 3 2 1. 총 9회 교환이 발생하고 두 번째 교환 직후 배열 A의 모습은 1 3 2 4 5이다.
예제 입력 2
5 10
2 5 1 4 3
예제 출력 2
-1
과정설명 :
교환 횟수 9가 K 보다 작으므로 -1을 출력한다.
만약 k==0이면(교환 X) 자신을 출력시키고 바로 프로그램을 종료한다.
교환이 일어날때마다 count를 1증가시킨다.
재귀가 일어나기때문에 count를 전역변수로 고정하여 값을 보존하고, 공유한다.
k번째 교환이 일어나는 시점에 배열을 출력하고 프로그램을 종료하여 이후, 연산을 실행하지 않게 한다.
이를 위해 count가 어디서 증가하는지 코드를 읽고 파악해야만 한다.
import java.io.*;
import java.util.Arrays;
import java.util.StringTokenizer;
import java.util.stream.Collectors;
public class Main {
static int count = 0; // 비교 횟수를 카운트하기 위한 변수
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringTokenizer st = new StringTokenizer(br.readLine()); // 첫번째 줄 입력받기
int n = Integer.parseInt(st.nextToken()); // 배열의 크기
int k = Integer.parseInt(st.nextToken()); // 교환할 횟수
int[] a = new int[n]; // 배열 a
st = new StringTokenizer(br.readLine()); // 두번째 줄 입력받기
for (int i = 0; i < n; i++) {
a[i] = Integer.parseInt(st.nextToken()); // 배열 a의 값들을 하나씩 넣어준다.
}
if (k == 0) { // k가 0인 경우 배열 a를 그대로 출력 (자신의 모습 그대로)
bw.write(String.join(" ", Arrays.stream(a)
.mapToObj(String::valueOf)
.toArray(String[]::new)) + "\n");
} else {
heapSort(a, n, k, bw); // 그 외의 경우 heapSort 메소드를 호출하여 k번 교환
}
bw.flush();
bw.close();
}
public static void heapify(int[] A, int k, int n, int k_value) { // 최소힙을 만들기 위한 heapify 메소드
int left = 2 * k; // 왼쪽 자식 노드의 인덱스
int right = 2 * k + 1; // 오른쪽 자식 노드의 인덱스
int smaller = k; // k의 인덱스
if (left <= n && A[left - 1] < A[smaller - 1]) { // 왼쪽 자식 노드가 존재하고 더 작은 경우
smaller = left; // smaller 변수를 왼쪽 자식 노드의 인덱스로 갱신
}
if (right <= n && A[right - 1] < A[smaller - 1]) { // 오른쪽 자식 노드가 존재하고 더 작은 경우
smaller = right; // smaller 변수를 오른쪽 자식 노드의 인덱스로 갱신
}
if (smaller != k) { // k의 값보다 자식 노드의 값이 더 작은 경우 교환
int temp = A[k - 1];
A[k - 1] = A[smaller - 1];
A[smaller - 1] = temp;
count += 1; // 비교 횟수 증가
if (count == k_value) { // 비교 횟수가 k와 같아지면 출력 후 코드 종료
String result = Arrays.stream(A)
.mapToObj(String::valueOf)
.collect(Collectors.joining(" "));
System.out.println(result); // k번째 교환된 배열 출력
System.exit(0);
}
heapify(A, smaller, n, k_value); // 자식 노드와 교환한 경우, 변경된 자식 노드를 기준으로 다시 heapify를 호출
}
}
public static void buildMinHeap(int[] A, int n, int k_value) { // 최소힙을 만들기 위한 buildMinHeap 메소드
for (int i = n / 2; i >= 1; i--) { // 마지막 단계의 부모 노드부터 heapify를 호출하여 최소힙 생성
heapify(A, i, n, k_value);
}
}
public static void heapSort(int[] A, int n, int k_value, BufferedWriter bw) throws IOException { // heapSort 메소드
buildMinHeap(A, n, k_value); // 최소힙 생성
for (int i = n; i >= 2; i--) { // 배열의 마지막 인덱스부터 시작하여 heapify를 호출하여 정렬 수행
int temp = A[0]; // 최소값과 맨 끝 값을 교환
A[0] = A[i - 1];
A[i - 1] = temp;
count += 1; // 비교 횟수 증가
if (count == k_value) { // 비교 횟수가 k와 같아지면 출력 후 코드 종료
String result = Arrays.stream(A)
.mapToObj(String::valueOf)
.collect(Collectors.joining(" "));
try {
bw.write(result + "\n"); // k번째 교환된 배열 출력
bw.flush();
bw.close();
System.exit(0);
} catch (Exception e) {// BufferedWriter 예외처리
e.printStackTrace();
}
}
heapify(A, 1, i - 1, k_value); // heapify를 호출하여 최소힙을 재구성
}
try {
bw.write("-1\n"); // 모든 교환이 이루어졌는데 여기까지 왔으면, -1 출력
bw.flush();
bw.close();
System.exit(0);
} catch (Exception e) { // BufferedWriter 예외처리
e.printStackTrace();
}
}
}
자바로 문제를 맞춘사람이 아직 나 포함 2명밖에 없었다.
다른 사람들이 보다 적은 시행착오를 통해 Heap정렬에 대해 이해하고 문제풀이에 도전할 수 있기를 바라며, 코드를 공유한다.