본문 바로가기

힙 정렬의 활성 구간과 확정 구간

최댓값을 끝에 옮긴 뒤 루트만 복구하며 전체 정렬을 완성합니다.

최댓값을 반복해서 꺼내면 내림차순으로 얻을 수 있습니다. 최대 힙의 최댓값을 목록 끝에 고정하면 오름차순 정렬도 만들 수 있습니다. 목표는 힙으로 유지할 활성 구간과 이미 정렬된 오른쪽 구간을 구분하는 것입니다. 앞 본문의 인덱스와 sift-down을 복습하세요.

개념과 약속

처음 전체를 최대 힙으로 만듭니다. 루트와 활성 구간의 마지막 원소를 교환하면 최댓값이 최종 위치에 갑니다. 마지막 칸을 활성 구간에서 제외한 뒤 루트의 새 값을 sift_down으로 내려 힙을 복구합니다. 다음 최댓값도 바로 왼쪽에 고정합니다.

end는 오른쪽 확정 구간 앞의 마지막 활성 위치입니다. sift_down(..., end)의 크기는 end 위치를 제외한 칸 수라서, 이제 확정된 최댓값을 다시 움직이지 않습니다. 이 구현은 작업 목록을 복사하지만 그 복사 안에서는 교환과 몇 개 변수로 진행합니다.

손으로 상태 추적하기

단계활성 힙오른쪽 확정 구간
[4,1,3,2]를 힙으로[4,2,3,1]없음
루트 4 고정·복구[3,2,1][4]
루트 3 고정·복구[2,1][3,4]
루트 2 고정[1][2,3,4]

직접 실행하기

아래 코드는 이 본문만으로 실행할 수 있습니다. lesson.py로 저장하고 첫 장에서 확인한 Python 명령으로 실행하세요.

def sift_down(values, root, size):
    while 2 * root + 1 < size:
        child = 2 * root + 1
        if child + 1 < size and values[child + 1] > values[child]:
            child += 1
        if values[root] >= values[child]:
            return
        values[root], values[child] = values[child], values[root]
        root = child


def build_max_heap(values):
    result = list(values)
    for root in range(len(result) // 2 - 1, -1, -1):
        sift_down(result, root, len(result))
    return result


def heap_sort(values):
    result = build_max_heap(values)
    for end in range(len(result) - 1, 0, -1):
        result[0], result[end] = result[end], result[0]
        sift_down(result, 0, end)
    return result


values = [4, 1, 3, 2]
print(heap_sort(values))
print(heap_sort([2, 2, 1]))
print(heap_sort([]))
print(values)

실행 결과:

[1, 2, 3, 4]
[1, 2, 2]
[]
[4, 1, 3, 2]

왜 맞고 언제 끝나는가

최대 힙의 루트는 활성 원소 전체의 최대입니다. 끝에 놓으면 그 위치는 올바르고, 다음 최대는 앞선 최대 이하라 오른쪽 구간의 순서도 보존됩니다. 교환 뒤 루트만 조건을 어길 수 있어 아래로 복구하면 충분합니다. 활성 크기가 하나씩 줄어 종료합니다.

비용과 적용 범위

힙 구성 O(n) 뒤 n번에 가까운 삭제·복구 O(log n)를 해서 최악 O(n log n)입니다. 내부 작업은 O(1) 보조 공간이지만 원본 보존 복사는 O(n)입니다. 먼 원소 교환으로 동률 순서를 바꿀 수 있어 일반적인 힙 정렬은 안정적이지 않습니다.

스스로 풀어 보기

  1. [3,1,2]에서 첫 최대를 고정한 뒤 활성 구간과 확정 구간을 쓰세요.
  2. 복구 크기를 전체 길이로 계속 주면 어떤 약속을 깨나요?

힌트

  1. 3과 마지막 2를 교환한 뒤 앞 두 칸만 봅니다.
  2. 정답 위치에 둔 값을 다시 힙에 포함합니다.

해설

  1. 활성 [2,1], 확정 [3]입니다. 앞 두 칸은 이미 최대 힙이라 추가 교환이 없습니다.
  2. 이미 확정된 큰 값이 다시 루트로 이동할 수 있습니다. 활성 범위를 줄이는 일은 최적화만이 아니라 정확성의 일부입니다.

흔한 오해

힙을 만든 직후 목록이 정렬됐다고 생각하면 안 됩니다. 루트만 최대이며 전체 순서는 반복적인 추출로 만듭니다.

복습과 다음 연결

다음 절의 그래프 표현에서는 자료 사이의 연결을 표현합니다. 정렬 선택에서 안정성·최악 시간·자료 제약을 함께 비교합니다.