제자리 힙 정렬: 배열 heapify와 확정 구간의 경계

·설명 약 6분·작성: 해비·블로그 소개

제자리 힙 정렬에서 힙 구간과 확정 구간은 어떻게 관리하나요?

제자리 힙 정렬은 입력 배열 자체를 최대 힙으로 만든 뒤, 최댓값을 오른쪽 끝에 하나씩 확정합니다. 아직 힙인 앞 구간과 정렬이 끝난 뒤 구간의 경계를 지키는 것이 핵심입니다.

본문 근거: Open Data Structures — 11.1.3 Heap-sort

읽기 전에 알아두면 좋아요

아래 개념이 익숙하면 본문을 바로 읽으세요. 낯선 개념만 링크에서 복습하면 됩니다.

개념과 예제는 실행하지 않아도 읽을 수 있습니다. 내 컴퓨터·터미널에서 직접 실행하는 과정은 선택 실습입니다.

과정 위치와 선택 복습 안내

과정 위치: 트리·그래프와 최단거리 · 알고리즘 개념

과정 위치는 이 글이 속한 묶음입니다. 이 단계의 모든 개념을 먼저 읽을 필요는 없습니다. 위에 연결한 개념 중 낯선 내용만 복습하세요. 문제의 조건·상태·갱신 순서는 익숙한 언어로 설명하면 됩니다.

JavaScript로 문법·실행 복습하기 (선택)

개념 글의 코드는 JavaScript 예시입니다. 문제 화면에서는 JavaScript·Python·Java·C++·C# 중 익숙한 언어의 함수를 작성하세요. 본문의 Node.js 입출력 예시는 로컬 실행을 위한 별도 안내입니다.

제자리 힙 정렬: 배열 heapify와 확정 구간의 경계 주제를 표현한 밝은 일러스트와 작은 곰 표지

제자리 힙 정렬은 입력 배열 자체를 최대 힙으로 만든 뒤, 최댓값을 오른쪽 끝에 하나씩 확정합니다. 아직 힙인 앞 구간과 정렬이 끝난 뒤 구간의 경계를 지키는 것이 핵심입니다.

선수 지식: 배열 인덱스·완전 이진 트리·힙 부모 자식 관계

  • 배열의 부모·자식 인덱스로 최대 힙 만들기
  • heapify가 아래에서 위로 진행하는 이유 설명하기
  • 동일 배열을 반환하는 제자리 정렬과 불안정성 이해하기

배열 앞부분을 최대 힙으로 해석합니다

최대 힙은 각 부모 값이 자식 값 이상인 완전 이진 트리입니다. 0부터 시작하는 배열에서는 부모 i의 왼쪽 자식이 2i+1, 오른쪽 자식이 2i+2입니다. 루트 0에는 최댓값이 있지만, 형제나 서로 다른 가지 전체가 오름차순인 것은 아닙니다. 그래서 힙을 만들었다는 사실과 정렬이 끝났다는 사실을 구분해야 합니다.

배열 5·4·3·1·10에서 size 4 앞부분만 최대 힙으로 해석하고 확정된 10을 제외하는 경계
힙은 배열 전체가 아니라 size 앞 구간입니다. 0번 5의 자식은 1번 4와 2번 3이며, 4번의 확정값 10은 트리와 비교 대상에서 제외합니다. 원본 보기 (새 탭)

이 예제의 siftDown은 현재 부모와 더 큰 자식을 비교합니다. 자식이 더 크면 두 값을 바꾸고 그 자식 위치에서 계속 내려갑니다. 작은 자식과 바꾸면 다른 큰 자식이 부모보다 커서 힙 조건이 남을 수 있으므로 반드시 두 자식 중 큰 쪽을 먼저 고릅니다. size는 배열 전체 길이가 아니라 현재 힙으로 다루는 앞 구간의 길이입니다. 자식 인덱스가 size 이상이면 이미 확정된 뒤 구간에 속하므로 읽지 않습니다.

마지막 부모부터 고쳐 heapify를 완성합니다

잎은 자식이 없으므로 혼자서 이미 힙입니다. 마지막 부모인 floor(N/2)-1부터 0까지 거꾸로 내려가며 siftDown을 적용하면, 현재 부모를 고칠 때 그 자식 아래는 이미 힙입니다. 자식 쪽의 조건을 먼저 완성하는 순서가 있어야 한 번 내려가는 작업만으로 현재 부분 트리도 고칠 수 있습니다. 빈 배열과 원소 하나에서는 시작 부모가 음수라 반복이 실행되지 않습니다.

4·10·3·5·1에서 마지막 부모 1을 확인하고 4와 10, 이어 4와 5를 교환해 10·5·3·4·1 최대 힙을 만드는 과정
자식 쪽이 이미 힙이 되도록 마지막 부모부터 확인합니다. 부모 0의 4를 큰 자식 10과 바꾼 뒤 다시 5와 바꾸어 아래로 내립니다. 원본 보기 (새 탭)
[4,10,3,5,1]의 최대 힙 만들기
작업 배열 설명
초기 4, 10, 3, 5, 1 마지막 부모는 인덱스 1
부모 1 확인 4, 10, 3, 5, 1 10이 자식 5·1 이상
부모 0 내리기 10, 5, 3, 4, 1 4가 10과 교환 후 5 아래로 이동

heapify를 각 원소의 삽입으로 구현하면 O(N log N) 상한이 자연스럽지만, 이 아래에서 위로 만드는 과정은 O(N)입니다. 깊게 내려갈 수 있는 높은 정점의 수가 적기 때문입니다. 대략 절반인 잎은 내려가지 않고, 그 위의 약 4분의 1은 한 층 정도만 내려갑니다. 높이별 비용을 합하면 선형 규모가 됩니다.

루트를 끝으로 옮기고 힙 길이를 하나 줄입니다

최대 힙의 루트는 아직 남아 있는 값 중 최댓값입니다. 루트와 현재 힙의 마지막 원소를 바꾸면 그 최댓값의 최종 위치가 확정됩니다. 이제 힙 길이를 하나 줄여 확정한 위치를 제외하고, 새 루트에만 siftDown을 수행합니다. 두 자식 아래는 원래의 힙 조건이 유지되므로 배열 전체를 다시 heapify할 필요가 없습니다. 오른쪽 구간은 이미 오름차순으로 확정되어 더는 변경하지 않습니다.

최대값을 오른쪽에 확정하며 활성 힙 길이가 4·3·2로 줄고 최종 1·3·4·5·10이 되는 힙 정렬 추적
경계 오른쪽의 값은 최종 위치가 정해졌습니다. siftDown은 size 앞부분만 비교하고 확정 구간을 다시 건드리지 않습니다. 원본 보기 (새 탭)

위 배열에서 10을 오른쪽 끝으로 옮기고 앞 네 칸을 고치면 [5,4,3,1 | 10]입니다. 다음에는 5가 확정되어 [4,1,3 | 5,10], 그다음 [3,1 | 4,5,10]이 됩니다. 마지막에는 [1,3,4,5,10]이 됩니다. 경계의 오른쪽 원소까지 siftDown의 비교 대상에 넣으면 확정한 큰 값이 다시 앞으로 올라오므로 size의 의미를 항상 확인해야 합니다.

같은 배열과 일정한 추가 공간을 유지합니다

heapify와 heapSort는 입력 배열의 원소를 직접 바꾸고 같은 배열 객체를 반환합니다. 새 힙에 모든 값을 삽입한 뒤 결과 배열로 꺼내는 방식은 이 글의 제자리 구현과 다릅니다. 반복형 siftDown에서 임시 값과 인덱스만 사용하므로 입력·출력 배열을 제외한 추가 공간은 O(1)입니다. 초기 heapify O(N) 뒤에 최대 N−1번, 한 번당 O(log N)의 하향 조정을 하므로 정렬의 최악 시간은 O(N log N)입니다.

원래 배열 4·10·3·5·1을 제자리 정렬한 뒤 입력 참조와 반환 참조가 같은 1·3·4·5·10 배열을 가리키는 계약
heapSort는 원소를 직접 바꾸고 입력과 같은 배열 객체를 반환합니다. 두 배열은 같은 객체의 호출 전후 상태이며 새 결과 배열을 만든다는 뜻이 아닙니다. 원본 보기 (새 탭)

이 구현은 빈칸 없이 채운 유한한 Number 배열을 받습니다. NaN은 크기 비교를 일관되게 할 수 없고 무한대·문자열·빈칸은 정한 계약 밖이므로 원소 교환 전에 거절합니다. 중복 값은 모두 보존하고 오름차순으로 정렬하지만 힙 정렬은 안정 정렬이 아닙니다. 같은 키에 표식이 있는 원소를 생각하면 멀리 떨어진 루트·끝 교환이 동률의 상대 순서를 바꿀 수 있습니다. 기존 순서를 유지해야 하는 레코드 정렬에는 그 요구를 별도로 반영해야 합니다.

전체 코드와 직접 실행

개념을 읽었다면 아래 예제를 직접 실행하며 상태 변화를 확인할 수 있습니다.

전체 JavaScript 코드와 실행 방법 펼치기

별도 패키지는 필요하지 않습니다. Node.js가 처음이라면 브라우저와 Node.js 시작 안내부터 확인하세요.

algorithm-in-place-heap-sort.mjs 파일을 만들고 아래 전체 코드를 저장합니다.

function siftDown(array, root, size) {
  while (2*root+1 < size) {
    let child = 2*root+1;
    if (child+1 < size && array[child+1] > array[child]) child++;
    if (array[root] >= array[child]) break;
    const temporary = array[root];
    array[root] = array[child];
    array[child] = temporary;
    root = child;
  }
}

function heapify(array) {
  if (!Array.isArray(array)) throw new TypeError('유한한 수의 배열이 필요합니다.');
  for (let i = 0; i < array.length; i++) {
    if (!Number.isFinite(array[i])) throw new TypeError('빈칸 없이 유한한 수만 넣으세요.');
  }
  for (let parent = Math.floor(array.length/2)-1; parent >= 0; parent--) {
    siftDown(array,parent,array.length);
  }
  return array;
}

function heapSort(array) {
  heapify(array);
  for (let end = array.length-1; end > 0; end--) {
    const temporary = array[0];
    array[0] = array[end];
    array[end] = temporary;
    siftDown(array,0,end);
  }
  return array;
}

const values = [4,10,3,5,1];
console.log(JSON.stringify(heapify(values)));
console.log(JSON.stringify(heapSort(values)));
console.log(JSON.stringify(heapSort([7,1,7,-2,0])));

파일이 있는 폴더의 터미널에서 실행합니다.

node algorithm-in-place-heap-sort.mjs

기대 출력

[10,5,3,4,1]
[1,3,4,5,10]
[-2,0,1,7,7]

비용과 적용 범위

유한한 Number가 빈칸 없이 들어 있는 길이 N의 배열을 직접 바꿉니다. 바닥부터 heapify는 O(N), 정렬 최악 시간 O(N log N), 반복형 구현의 추가 공간 O(1)입니다. 같은 배열을 반환하고 중복 값은 보존하지만 안정 정렬은 아닙니다.

배열 4·10·3·5·1을 최대 힙 10·5·3·4·1로 만든 뒤 정렬하는 단계와 O(N)·O(N log N) 비용
바닥부터 heapify는 O(N), 전체 정렬의 최악 시간은 O(N log N)입니다. 입력은 빈칸 없는 유한 Number 배열이며 반복형 구현의 추가 공간은 O(1)입니다. 중복은 보존하지만 안정 정렬은 아닙니다. 원본 보기 (새 탭)

직접 확인해 보기

최대 힙 [5,4,3,1]의 첫 정렬 단계 후, 앞 세 칸의 힙과 오른쪽 확정 구간은 무엇일까요? siftDown의 size는 얼마일까요?

최대 힙 5·4·3·1의 초기 배열과 트리, 첫 정렬 단계 뒤 힙·확정 구간·size를 묻는 빈칸
주어진 최대 힙에서 첫 정렬 단계를 손으로 수행해 보세요. 연산 후 힙·확정 구간·size는 빈칸에 직접 적도록 남겼습니다. 원본 보기 (새 탭)
답과 이유 확인

루트 5와 마지막 1을 바꾸고 앞 세 칸만 고치면 [4,1,3 | 5]입니다. size는 3이며, 확정된 인덱스 3의 5는 더 이상 비교에 포함하지 않습니다.

참고 자료

  • Open Data Structures — 11.1.3 Heap-sort — 배열 힙 정렬의 최대 힙 구성, 제자리 정렬, 선형 heapify와 O(N log N) 정렬 원리. Java 구현을 복사하지 않고 독립적인 JavaScript 학습 예제를 작성했습니다.

이 글이 도움이 되었나요?

조회 중

이어서 공부하기

체크포인트: 예제의 상태 변화를 손으로 추적하고 아래 문제에 적용하세요. 값이 없는 경우와 원소 하나인 경우도 설명해 보세요. 층별 방문 순서와 최솟값 추출을 추적하고, 다익스트라의 비음수 조건과 오래된 후보를 설명하세요.

다음 추천 · 문제: 최소 힙 연습: 동률 순서를 지키는 작업 스케줄러

같은 주제의 문제에 적용하며 방금 배운 개념을 확인합니다.

권장 순서는 학습 가이드와 같습니다. 다른 순서는 접힌 참고 목록에서 확인할 수 있습니다. 읽음 표시는 숙달이나 채점 결과가 아닙니다.

원래 과정 목록 순서 (선택)

읽음 기록

읽음 — / 전체 30개 · 필수 0개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기