Krafton Jungle/3. TIL

[WEEK01] 힙 소트

munsik22 2025. 3. 18. 13:52

힙 정렬

heap이란 정렬된 이진 트리ordered binary tree를 의미한다.

  • 최대 힙 : 상위 노드(부모)의 값 > 하위 노드(자식)의 값
  • 최소 힙 : 상위 노드(부모)의 값 < 하위 노드(자식)의 값

[출처] https://javascript.plainenglish.io/real-world-uses-cases-for-heaps-e57edbeb7da3

  • build-max-heap : 정렬되지 않은 배열에서 최대 힙을 만들어 낸다.
  • heapify : build-max-heap과 비슷하지만, 배열의 일부분이 정렬된 상태라고 가정한다.

위와 같이 정렬을 하도록 지정된 배열에 대해 다음 단계를 수행한다.

  1. Max heap 만들기
  2. 가장 큰 item 제거하기
  3. item을 정렬된 부분배열에 넣기

최대 힙으로 만들기
최대값과 최소값 바꾸고 트리에서 최대값 제거하기
최대 힙으로 만들기
최대값 제거
최대 힙으로 만들기
최대값 제거
최대 힙으로 만들기
최대값 제거
최대 힙으로 만들기
최대값 제거
정렬 완료!

def left(i):
    return 2*i

def right(i):
    return 2*i + 1

def parent(i):
    return i//2

def max_heapify(a, heap_size, i):
    l = left(i)
    r = right(i)

    largest = i 

    if l < heap_size and a[l] > a[i]:
        largest = l
    
    if r < heap_size and a[r] > a[largest]:
        largest = r 
    
    if largest != i:
        # swap elements
        a[i], a[largest] = a[largest], a[i]
        max_heapify(a, heap_size, largest)

def build_max_heap(a):
    heap_size = len(a)

    for i in range(heap_size//2, 0, -1):
        max_heapify(a, heap_size, i)

def heap_sort(a):
    build_max_heap(a)

    for i in range(len(a)-1, 1, -1):
        # swap elements
        a[i], a[1] = a[1], a[i]

        # after the swap the last element is now sorted, but the new root may break the max-heap condition
        # fix it by calling max-heapify with a smaller heap size (sorted elements are out of the picture)
        max_heapify(a, i, 1)

def main():
    a = [None, 99, 0, 5, 20, 123, 0, -1, 72, 21, 22, 13, 8, 7, 67, 29, 1, 2, 4]
    heap_sort(a)
    print(a[1:])

    a = [None, 3, 9, 2, 1]
    heap_sort(a)
    print(a[1:])

main()

[출처] 4분 만에 힙 정렬


heapify

def heafity(arr, left, right):
    tmp = arr[left]
    parent = left
    while parent < (right+1) // 2:
        cl = parent * 2 + 1
        cr = parent * 2 + 2
        child = cr if cr <= right and arr[cr] > arr[cl] else cl
        if tmp >= arr[child]: break
        arr[parent] = arr[child]
        parent = child
    arr[parent] = tmp

🔍 코드 분석

  • arr: 힙을 구성할 배열
  • left: 힙을 조정할 루트 노드 인덱스
  • right: 힙의 마지막 인덱스 (배열 크기가 n이라면 right = n - 1)

1. 현재 루트 노드를 임시 변수로 저장

  • tmp: 루트 노드의 값을 저장 (나중에 적절한 위치에 다시 배치)
  • parent: 현재 부모 노드의 인덱스 (초기값은 left)

2. 왼쪽 자식이 존재하는 동안 반복

  • (right+1) // 2는 마지막 내부 노드의 개수를 나타냄
    (즉, 부모 노드가 존재할 수 있는 최대 위치)
  • parent < (right+1) // 2이면 현재 부모 노드가 리프 노드가 아님, 따라서 반복

3. 왼쪽과 오른쪽 자식 인덱스 계산

  • cl: 왼쪽 자식의 인덱스
  • cr: 오른쪽 자식의 인덱스

4. 더 큰 자식 선택

  • cr <= right: 오른쪽 자식이 배열 범위 내에 있는지 확인
  • arr[cr] > arr[cl]: 오른쪽 자식이 왼쪽 자식보다 크다면 cr을 선택, 아니면 cl 선택
  • 즉, 더 큰 값을 가진 자식을 선택하여 child로 지정

5. 부모 노드가 더 크면 종료

  • 부모(tmp)가 선택한 자식보다 크거나 같으면 힙 속성이 유지된 상태 → 종료

6. 부모를 자식의 값으로 교체 후 인덱스 변경

  • 부모 노드의 값을 자식 노드의 값으로 변경
  • parent를 child로 업데이트하여 다음 반복에서 검사할 위치를 변경
    → 아래로 내려가면서 힙 구조를 조정하는 과정

7. 최종 위치에 저장

  • tmp를 최종적으로 적절한 위치에 배치

🚀 실행 예제

(Ex) [4, 10, 3, 5, 1] 배열을 힙으로 만드는 과정

i = 0, left = 1, right = 4
arr = [4, 10, 3, 5, 1]

1. tmp = 4, parent = 0
2. cl = 1, cr = 2
3. arr[1] = 10, arr[2] = 3 → child = 1 (더 큰 값 선택)
4. tmp(4) < arr  → 교환
   arr = [10, 10, 3, 5, 1] (임시적으로 중복된 값)
5. parent = 1 (이동)
6. cl = 3, cr = 4
7. arr[3] = 5, arr[4] = 1 → child = 3
8. tmp(4) < arr  → 교환
   arr = [10, 5, 3, 5, 1]
9. parent = 3 (이동), 더 이상 자식 없음 → 종료
10. 최종적으로 arr[3] = tmp(4) → arr = [10, 5, 3, 4, 1]​

heap_sort

def heap_sort(arr):
    n = len(arr)
    
    # 1. 최대 힙 생성 (힙 구조 만들기)
    for i in range((n-1)//2, -1, -1):
        heafity(arr, i, n-1)
    
    # 2. 정렬 과정 (최대값을 하나씩 꺼내 정렬)
    for i in range(n-1, 0, -1):
        arr[0], arr[i] = arr[i], arr[0]  # 최대값을 배열의 끝으로 이동
        heafity(arr, 0, i-1)

    return arr

🔍 코드 분석

1. 최대 힙을 구성

  • (n-1)//2부터 0까지 역순으로 heafity를 호출해 배열을 최대 힙으로 변환
  • (n-1)//2는 마지막 내부 노드의 인덱스로, 여기서부터 루트까지 내려가며 힙 속성을 유지

2. 정렬 과정

  • 루트(최대값)를 배열의 끝과 교환
  • 교환 후, 남은 요소들(i-1까지)에 대해 다시 heafity를 호출하여 힙 속성을 유지

🚀 실행 예제

arr = [4, 10, 3, 5, 1]
heap_sort(arr)
print(arr)
[10, 5, 3, 4, 1] → [1, 5, 3, 4, 10]
[5, 1, 3, 4, 10] → [4, 1, 3, 5, 10]
[4, 1, 3, 5, 10] → [3, 1, 4, 5, 10]
[3, 1, 4, 5, 10] → [1, 3, 4, 5, 10]

📌 정리

  • 두 단계로 진행됨
    1. 배열을 최대 힙으로 변환
    2. 루트(최대값)과 배열 끝을 교환 후, 다시 힙 속성을 유지
  • 시간 복잡도: O(n log n)
    • 힙 구성: O(n)
    • 정렬 과정에서 heafity 호출: O(log n) × n번 → O(n log n)
  • 제자리 정렬(in-place sort), 추가 메모리 사용 없음