일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 티스토리 쿠키 삭제
- 리눅스
- 스프링부트핵심가이드
- d
- 처음 만나는 AI수학 with Python
- 알파회계
- baeldung
- 코드로배우는스프링부트웹프로젝트
- 페이징
- 스프링 시큐리티
- GIT
- ㅒ
- 네트워크 설정
- iterator
- 자바편
- 자료구조와 함께 배우는 알고리즘 입문
- 서버설정
- 자료구조와함께배우는알고리즘입문
- 이터레이터
- 구멍가게코딩단
- 친절한SQL튜닝
- 처음 만나는 AI 수학 with Python
- /etc/network/interfaces
- 데비안
- Kernighan의 C언어 프로그래밍
- network configuration
- 목록처리
- 코드로배우는스프링웹프로젝트
- 선형대수
- resttemplate
- Today
- Total
bright jazz music
힙 정렬(heap sort) 본문
힙 정렬은 최댓값, 최솟값을 찾는 데 특화된 정렬 방법이다. 해결해야 할 문제가 최대, 최솟값을 찾아내는 것이라면 힙 정렬을 사용해야 할 수도 있다.
힙(heap):
힙은 최댓값 및 최솟값을 찾아내는 연산을 빠르게 하기 위해 고안된 완전 이진트리(complete binary tree)를 기본으로 한 자료구조(tree-based structure)이며 힙 속성을 만족한다.
완전 이진 트리(complete binary tree):
완전 이진트리는 각각의 노드가 최대 "두 개"의 자식 노드를 가지는 트리 자료구조이다. 가장 위에 존재하는 노드를 루트 노드(root node)라고 한다.
이진트리에는 레벨이 존재한다. 루트 노드가 위치하는 가장 상단 레벨부터 Level 0 이다. 내려갈수록 레벨이 증가한다.
이진트리에도 종류가 많지만 아래의 조건을 충족해야만 완전 이진 트리로 분류될 수 있다.
- 마지막 레벨을 제외하고 모든 노드가 꽉 차있어야 한다.
- 노드는 왼쪽에서 오른 쪽으로 채워져야만 한다.
만약 왼쪽이 빈 상태로 오른쪽 자식 노드만 채워진 상태라면 완전 이진 트리가 될 수 없다.
힙 정렬의 순서
- 1단계: 정렬해야 할 n개의 원소를 사용해 최대힙(max heap) 형태의 완전 이진 트리를 만든다.
- 2단계: 한 번에 하나씩 원소를 힙에서 꺼낸 후 배열의 뒤부터 저장한다.
- 3단계: 2단계를 반복하면, 최댓값부터 값이 감소하는 순서로 배열에 저장된다.
힙에는 최대힙과 최소힙이 존재한다.
최대힙의 특징
- 각 노드의 값이 해당 노드의 자식 노드들의 값보다 크거나 같다.
- 루트 노드에는 전체 힙에서 가장 큰 값이 위치한다.
- 우선순위 큐에서는 가장 큰 값이 우선적으로 처리되어야 할 때 사용된다.
최소힙의 특징
- 각 노드의 값이 해당 노드의 자식 노드들의 값보다 작거나 같다.
- 루트 노드에는 전체 힙에서 가장 작은 값이 위치한다.
- 우선순위 큐에서는 가장 작은 값이 우선적으로 처리되어야 될 때 사용된다.
최대힙과 최소힙은 정렬과 방향의 차이가 존재한다. 그러므로 만약 힙 정렬을 할 때, 내림차순으로 정렬이 필요하다면 최대 힙, 오름차순으로 정렬이 필요하다면 최소 힙을 사용할 수 있다.
힙 정렬의 시간 복잡도
힙 정렬은 시간복잡도가 좋다. 모든 경우에 O(n log n)의 시간 복잡도를 가진다. 또한 이진 트리 노드의 특성을 활용해 최댓값, 최솟값을 추출하는 데 유용하다.
힙 정렬의 공간복잡도
힙 정렬의 공간 복잡도는 2가지 패턴이 나타날 수 있다.
- In-place 힙 정렬: 추가적인 공간을 사용하지 않고 입력 배열 내에서 정렬을 수행하는 방식. 이 경우 힙 정렬의 공간 복잡도는 O(1)이다.
- Out-place 힙 정렬: 추가적인 입력 배열을 생성하여 정렬을 만들고 수행한다. 이 경우 추가 생성 배열의 크기는 입력 배열 크기에 비례한다.
힙 정렬은 불안정 정렬이다. 대부분 힙을 구성할 때 기존 배열의 순서와 무관하게 최대 힙을 구성하는 데 중점을 둔다. 이 과정에서 동일 값의 순서 변경이 발생할 여지가 있다. 또한 루트 노드 선택 과정에서 가장 큰 값 또는 작은 값을 택하게 되는데 동일한 값이 여러 개인 경우, 상대적인 순서가 유지되기 어렵다. 이러한 이유에서 안정 정렬이 필요한 경우라면 힙정렬은 좋은 선택지가 아닐 수 있다.
----------------------
1. 기본 개념
- 완전 이진트리 구조를 사용
- 부모-자식 간의 대소관계가 정해짐
* 최대 힙: 부모 > 자식
* 최소 힙: 부모 < 자식
2. 구현 시 알아야 할 공식
부모 노드 인덱스 = (자식 인덱스 - 1) // 2
왼쪽 자식 인덱스 = (부모 인덱스 * 2) + 1
오른쪽 자식 인덱스 = (부모 인덱스 * 2) + 2
3. 정렬 과정 (최대 힙 기준)
- 배열을 힙으로 변환(heapify)
- 루트(최댓값)를 배열 마지막과 교환
- 힙 크기 1 감소
- 다시 heapify
- 반복
4. 시간 복잡도
- 평균: O(n log n)
- 최악: O(n log n)
- 최선: O(n log n)
5. 공간 복잡도
- O(1) : 추가 메모리 거의 불필요
6. 특징
- 불안정 정렬 (같은 값의 순서 보장 X)
- 제자리 정렬 (추가 메모리 필요 X)
- 최악의 경우에도 O(n log n) 보장
7. 장단점
장점:
- 추가 메모리 필요 없음
- 최악의 경우에도 성능 보장
단점:
- 불안정 정렬
- 캐시 효율이 나쁨
- 같은 O(n log n) 알고리즘 중 실제로는 퀵소트보다 느림
8. 주 사용처
- 우선순위 큐 구현
- 메모리가 제한적일 때
- 최악의 경우에도 성능 보장 필요할 때
9. 기본형(최대힙)
function heapSort(arr) {
const n = arr.length;
// 1. 초기 힙 만들기
for(let i = Math.floor(n/2 - 1); i >= 0; i--) {
heapify(arr, n, i);
}
// 2. 정렬하기
for(let i = n-1; i > 0; i--) {
// 최댓값(루트)를 맨 뒤로 보내기
[arr[0], arr[i]] = [arr[i], arr[0]];
// 힙 다시 만들기
heapify(arr, i, 0);
}
return arr;
}
function heapify(arr, n, i) {
let largest = i;
let left = 2 * i + 1;
let right = 2 * i + 2;
// 왼쪽 자식이 더 크면
if(left < n && arr[left] > arr[largest]) {
largest = left;
}
// 오른쪽 자식이 더 크면
if(right < n && arr[right] > arr[largest]) {
largest = right;
}
// 교환이 필요하면
if(largest !== i) {
[arr[i], arr[largest]] = [arr[largest], arr[i]];
heapify(arr, n, largest);
}
}
히피파이(힙을 만드는 과정)에 대한 도표화
배열 [4, 10, 3, 5, 1]을 예시로 heapify 과정을 설명
초기상태
4(0)
/ \
10(1) 3(2)
/ \
5(3) 1(4)
1단계 (i=1일 때):
// left=3, right=4, largest=1 시작
4(0)
/ \
10(1) 3(2)
/ \
5(3) 1(4)
// 10과 자식들 비교 후 변화 없음
2단계 (i=0일 때):
// left=1, right=2, largest=0 시작
4(0)
/ \
10(1) 3(2)
/ \
5(3) 1(4)
// 4 < 10이므로 largest = 1
// swap 발생
10(0)
/ \
4(1) 3(2)
/ \
5(3) 1(4)
// 재귀: 4의 위치에서 다시 heapify
10(0)
/ \
5(1) 3(2)
/ \
4(3) 1(4)
//최종 최대힙
10(0)
/ \
5(1) 3(2)
/ \
4(3) 1(4)
주의할 것은 heapify 함수를 사용한다고 해서 배열이 정렬된다고 생각해서는 안된다는 것이다. 이 함수는 배열을 이진 트리 구조로 만들어 부모와 자식의 관계를 정리해 주는 것 뿐이다. 이를 배열의 구조로 봤을 때는 전혀 정렬된 것이 아니다.
예를 들어 최대힙으로 만든다고 하면, 루트 노드에는 가장 큰 값이 오게 되고, 그 아래부터 계속해서 부모 노드가 자식 노드보다 큰 값을 가지는 구조가 만들어지게 된다. 그러나 이를 배열의 구조에서 바라봤을 때 내림차순으로 정렬되어 있지는 않은 것이다. 따라서 heapSort함수에서 힙의 정렬을 수행하게 되는 것이다.
예제1)
/*
100개의 보물을 찾은 모험가
보물 상자 겉면에 쓰여있는 숫자에 따라서 숫자가 클수록 더 귀한 보물이 담겨있다
100개의 보물 상자 중 가장 귀한 보물상자들만 챙기려면?
*/
let treasureBoxs = [2, 5, 20, 602, 49, 856, 10, 1, 5, 298, 457, 21, 790, 988
]
function heapify(arr, n, i) {
// 배열과, 배열의 길이와, 인덱스를 전달받는다.
// 루트 노드
let largest = i;
// 자식노드 구성. 이진트리라 양쪽이 있으므로 2를 곱해서 +1 해줘야 함. 1-0-2, 3-1-4, 5-2-6
let left = 2 * i + 1; // 왼쪽 자식
let right = 2 * i + 2; // 오른쪽 자식
// 왼쪽이 배열의 길이보다 작고, 배열의 왼쪽 인덱스 값이 현재 최댓값보다 크다면, 최댓값에 왼쪽 값을 할당
if ( left < n && arr[left] > arr[largest]) {
largest = left;
}
// 오른쪽이 배열의 길이보다 작고, 배열의 오른쪽 인덱스 값이 현재 최댓값보다 크다면, 최댓값에 오른쪽 값을 할당
if ( right < n && arr[right] > arr[largest]) {
largest = right;
}
// 만약 최댓값이 파라미터로 넘어온 i값과 다르다면, 배열의 i번째 값과 최댓값을 바꾼다.
if (largest != i) {
let swap = arr[i];
arr[i] = arr[largest];
arr[largest] = swap;
// [arr[i], arr[largest]] = [arr[largest], arr[i]];]]
// 함수를 재귀 호출하여 힙을 다시 구성한다.
heapify(arr, n, largest);
}
}
function heapSort(arr) {
let n = arr.length;
// 1. 배열의 길이를 반으로 나눠서 버림한 값에서 1을 뺀 값을 인덱스로 지정. i는 0이상이고 계속해서 감소.
//(non-leaf) 노드부터 시작해서 최대 힙 구성. n/2-1부터 시작하는 이유: 리프 노드는 자식이 없어서 heapify가 필요 없음
for (let i = Math.floor(n / 2) - 1; i>= 0; i-- ) {
// 조건을 만족할 때까지 힙을 생성
heapify(arr, n, i);
}
// 힙 정렬과정
// 인덱스를 배열길이-1로 설정하고, 인덱스가 0보다 크며 인덱스가 순회 시마다 감소
for (let i = n - 1; i > 0; i--) {
let temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
//[arr[0], arr[i]] = [arr[i], arr[0]];]
heapify(arr, i, 0);
}
}
heapSort(treasureBoxs);
console.log(treasureBoxs)
console.log(treasureBoxs.reverse())
/*
[
1, 2, 5, 5, 10, 20,
21, 49, 298, 457, 602, 790,
856, 988
]
[
988, 856, 790, 602, 457, 298,
49, 21, 20, 10, 5, 5,
2, 1
]
*/
/*
자식 노드 찾기
왼쪽 자식 = 2 * i + 1
오른쪽 자식 = 2 * i + 2
부모 노드 찾기
부모 = Math.floor((i - 1) / 2)
// 인덱스 3(값 2)의 부모 찾기
Math.floor((3 - 1) / 2) = 1 // 값 5
------
if ( left < n && arr[left] > arr[largest]) {
largest = left;
}
left < n 조건이 필요한 이유
n은 배열의 길이
배열 범위를 벗어나지 않도록 체크
자식 노드가 실제로 존재하는지 확인
arr[left] > arr[largest] 조건이 필요한 이유
최대 힙의 규칙: 부모는 자식보다 커야 함
largest는 현재 검사 중인 부모 노드의 인덱스
왼쪽 자식이 더 크면 교환이 필요
오른쪽에 대한 조건문도 같은 이유이다.
그러나 완전 이진트리의 특성상 왼쪽부터 채워져야 하므로 왼쪽을 검사하는 로직이 먼저 등장해야 한다.
*/
예제2.
/*
조금 까다로운 손님을 만난 바텐더
- 손님은 가장 도수가 높은 위스키와 가장 낮은 위스키 한잔씩을 원한다.
- 이 가게에는 위스키의 종류가 100종류가 되며 각각 다른 도수를 가지고 있다.
- 술이 너무 많았기 때문에 손님은 바텐더에게 30초만 살펴보고 그 중 최고도수와 최저도수 위스키를 요청했다.
- 바텐더는 두 술의 도수를 비교해 순서를 변경하는 데 매번 1초가 걸렸다.
*/
// 위스키 배열 생성 (1~100 사이 소수점 포함)
let whiskeys = Array.from({ length: 100 }, () => Math.random() * 100 + 1);
console.log("원본 위스키 배열:", whiskeys);
let timeLeft = 30; // 제한시간
let comparisons = 0; // 비교 횟수 추적
// 최대 힙 구성을 위한 heapify 함수
function maxHeapify(arr, n, i) {
if (timeLeft <= 0) return; // 시간 초과시 중단
let largest = i;
let left = 2 * i + 1;
let right = 2 * i + 2;
if (left < n) {
comparisons++;
timeLeft--;
if (arr[left] > arr[largest]) {
largest = left;
}
}
if (right < n && timeLeft > 0) {
comparisons++;
timeLeft--;
if (arr[right] > arr[largest]) {
largest = right;
}
}
if (largest !== i && timeLeft > 0) {
[arr[i], arr[largest]] = [arr[largest], arr[i]];
maxHeapify(arr, n, largest);
}
}
// 힙 정렬 함수
function findMaxWhiskey(arr) {
const n = arr.length;
// 최대 힙 구성
for (let i = Math.floor(n / 2) - 1; i >= 0 && timeLeft > 0; i--) {
maxHeapify(arr, n, i);
}
return arr[0]; // 루트가 최댓값
}
// 배열 복사해서 최댓값 찾기
let arrForMax = [...whiskeys];
const maxWhiskey = findMaxWhiskey(arrForMax);
// 남은 시간으로 최솟값 찾기 시도
// 최소 힙 구성을 위한 heapify 함수
function minHeapify(arr, n, i) {
if (timeLeft <= 0) return;
let smallest = i;
let left = 2 * i + 1;
let right = 2 * i + 2;
if (left < n) {
comparisons++;
timeLeft--;
if (arr[left] < arr[smallest]) {
smallest = left;
}
}
if (right < n && timeLeft > 0) {
comparisons++;
timeLeft--;
if (arr[right] < arr[smallest]) {
smallest = right;
}
}
if (smallest !== i && timeLeft > 0) {
[arr[i], arr[smallest]] = [arr[smallest], arr[i]];
minHeapify(arr, n, smallest);
}
}
function findMinWhiskey(arr) {
const n = arr.length;
// 최소 힙 구성
for (let i = Math.floor(n / 2) - 1; i >= 0 && timeLeft > 0; i--) {
minHeapify(arr, n, i);
}
return arr[0]; // 루트가 최솟값
}
let minWhiskey;
if (timeLeft > 0) {
let arrForMin = [...whiskeys];
minWhiskey = findMinWhiskey(arrForMin);
}
console.log("수행된 비교 횟수:", comparisons);
console.log("남은 시간:", timeLeft);
console.log("가장 높은 도수의 위스키:", maxWhiskey);
console.log("가장 낮은 도수의 위스키:", minWhiskey || "시간 부족으로 찾지 못함");
아래처럼 단순 비교가 나을 수 있다.
let whiskeys = Array.from({ length: 100 }, () => Math.random() * 100 + 1);
let availableTime = 30;
let comparisons = 0;
// 단순하게 한 번의 순회로 최대값과 최소값을 동시에 찾기
function findMinMax(arr) {
let max = arr[0];
let min = arr[0];
for(let i = 1; i < arr.length && comparisons < availableTime; i++) {
comparisons++; // max 비교
if(arr[i] > max) {
max = arr[i];
}
comparisons++; // min 비교
if(arr[i] < min) {
min = arr[i];
}
}
return { max, min };
}
const result = findMinMax(whiskeys);
console.log(`비교 횟수: ${comparisons}`);
console.log(`도수가 가장 높은 위스키: ${result.max}`);
console.log(`도수가 가장 낮은 위스키: ${result.min}`);
'Algorithm&Data structure > JS alg.' 카테고리의 다른 글
Array.prototype.sort() 메서드 (0) | 2024.10.30 |
---|---|
기수정렬(Radix sort) (0) | 2024.10.28 |
퀵 정렬(Quick Sort) 일반형 (0) | 2024.10.25 |
합병정렬 일반형 (2) | 2024.10.23 |
삽입정렬 일반형 (0) | 2024.10.21 |