퀵 정렬: 첫 피벗의 자리를 확정하고 양쪽을 정렬하기

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

퀵 정렬은 첫 피벗의 자리를 확정한 뒤 어떤 구간을 다시 정렬하나요?

퀵 정렬은 피벗 하나의 최종 위치를 먼저 확정합니다. 작은 값은 왼쪽으로, 크거나 같은 값은 오른쪽으로 나눈 뒤 피벗을 제외한 두 구간만 다시 처리합니다.

적용 조건·범위: 분석 조건: 첫 원소 피벗과 경계 인덱스로 작은 값 구간을 모으는 분할입니다. 평균은 서로 다른 키가 임의 순서인 가정이며, 정렬된 입력·역순·모두 같은 값에서 최악이 가능합니다. 작은 쪽만 재귀로 처리하여 최악 시간과 별개로 스택 깊이를 제한합니다.

본문 근거: Princeton Algorithms — Quicksort

읽기 전에 알아두면 좋아요

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

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

과정 위치와 선택 복습 안내

과정 위치: 정렬의 원리와 불변식 · 알고리즘 개념

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

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

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

퀵 정렬: 첫 피벗의 자리를 확정하고 양쪽을 정렬하기 주제를 표현한 밝은 일러스트와 작은 곰 표지

퀵 정렬은 피벗 하나의 최종 위치를 먼저 확정합니다. 작은 값은 왼쪽으로, 크거나 같은 값은 오른쪽으로 나눈 뒤 피벗을 제외한 두 구간만 다시 처리합니다.

선수 지식: 재귀, 배열 구간, 값 교환, 정렬의 불변식

  • 분할이 보장하는 것과 아직 보장하지 않는 것을 구분합니다.
  • 첫 피벗이 정렬된 입력에서 만드는 최악의 분할을 이해합니다.
  • 작은 구간만 재귀로 처리하여 호출 깊이를 제한합니다.

피벗보다 작은 값의 구간을 앞쪽에 모읍니다

[3, 1, 4, 2]에서 첫 값 3을 피벗으로 고릅니다. 피벗을 잠시 첫 자리에 두고 나머지를 왼쪽부터 봅니다. boundary는 지금까지 모은 작은 값 구간의 끝입니다. 새 값이 3보다 작으면 boundary를 한 칸 늘리고 그 자리와 새 값의 자리를 바꿉니다. 크거나 같은 값이면 작은 구간에 넣지 않고 다음 값으로 넘어갑니다.

피벗 3의 작은 값 구간과 분할 뒤 아직 정렬되지 않은 왼쪽 구간
분할은 피벗의 자리를 확정합니다. 왼쪽 [2,1]은 모두 3보다 작지만 아직 정렬되지 않았으므로 따로 처리해야 합니다. 원본 보기 (새 탭)

스캔 도중에는 첫 자리 다음부터 boundary까지가 피벗보다 작은 구간입니다. boundary 다음부터 이미 살핀 자리까지는 피벗보다 크거나 같은 구간입니다. 아직 살피지 않은 뒤쪽은 어떤 순서라도 괜찮습니다. 작은 값을 새로 만나면 큰 구간 맨 앞과 교환하여 이 구분을 유지합니다. 한 번의 교환은 정렬 전체가 아니라 구간의 조건을 지키는 동작입니다.

스캔이 끝나면 첫 자리의 피벗을 boundary와 바꿉니다. 왼쪽에는 모두 작은 값, 오른쪽에는 모두 크거나 같은 값이 있으므로 피벗은 최종적으로 들어갈 한 위치에 놓입니다. 양쪽 구간 안의 순서는 아직 섞여 있을 수 있어 각각 정렬해야 합니다. 병합 정렬과 달리 두 결과를 다시 합치는 별도 병합 단계는 없습니다.

작은 값의 경계를 움직인 뒤 피벗을 놓습니다

첫 피벗 3을 기준으로 작은 값의 경계를 옮긴 뒤 인덱스 2를 확정하는 네 단계
피벗 3의 자리를 정한 뒤에도 왼쪽 [2,1]은 따로 정렬합니다. 원본 보기 (새 탭)
[3, 1, 4, 2], 첫 피벗 3의 분할
살피는 값 boundary 배열 이유
1 0 → 1 [3, 1, 4, 2] 1 < 3이므로 작은 구간에 포함
4 1 유지 [3, 1, 4, 2] 4는 작지 않으므로 넘어감
2 1 → 2 [3, 1, 2, 4] 작은 값 2를 인덱스 2로 이동
피벗 배치 2 확정 [2, 1, 3, 4] 피벗을 작은 구간의 끝과 교환

분할 뒤 [2, 1]은 여전히 정렬되지 않았지만 둘 다 3보다 작습니다. 이 구간만 같은 방식으로 정렬하면 [1, 2]가 됩니다. 오른쪽 [4]는 한 원소라 추가 작업이 없습니다. 피벗 인덱스를 다음 구간에 다시 포함하지 않아야 매번 문제 크기가 줄어듭니다.

첫 피벗의 최악 시간과 호출 깊이를 구분합니다

이미 정렬된 [1, 2, 3, 4]에서 첫 피벗 1보다 작은 값은 없습니다. 분할 뒤 왼쪽은 비고 오른쪽에 3개가 남습니다. 다음에도 같은 일이 반복되어 비교 수가 3 + 2 + 1이 됩니다. 역순이나 같은 값만 있는 배열에서도 이 분할 방식은 불균형해질 수 있어 최악 시간은 O(n²)입니다. 첫 값을 고정해서 고르는 코드에 항상 빠르다는 보장은 없습니다.

첫 피벗의 이차 비교와 작은 쪽만 재귀하는 로그 스택 깊이의 구분
작은 구간만 재귀하고 큰 구간은 반복문으로 처리하면 호출 깊이를 제한할 수 있습니다. 첫 피벗의 불균형한 분할로 생기는 최악 비교 수는 줄지 않습니다. 원본 보기 (새 탭)

양쪽을 모두 그대로 재귀 호출하면 이런 입력에서 깊이도 O(n)이 되어 호출 스택 한계에 부딪힐 수 있습니다. 예제는 양쪽 길이를 비교하여 작은 쪽만 재귀 호출하고 큰 쪽은 while문으로 이어갑니다. 재귀로 내려가는 구간은 직전 구간의 절반 이하라 동시에 살아 있는 호출 깊이는 O(log n)으로 제한됩니다. 빈 구간이나 한 원소는 바로 끝납니다.

이 변경은 스택 사용을 줄여도 최악 비교 수를 줄이지는 않습니다. 큰 구간을 반복문으로 처리하더라도 n – 1, n – 2 크기의 스캔을 계속 해야 하기 때문입니다. 평균 O(n log n)은 원소가 서로 다른 임의 순서의 입력처럼 피벗 순위가 골고루 나타나는 가정에서 말합니다. 중복이 많은 입력에는 삼방향 분할, 보장된 시간에는 다른 정렬 전략을 고려할 수 있습니다.

숫자 계약과 실제 사용하는 공간을 확인합니다

함수는 빈 칸 없는 유한 Number 배열만 받아 복사본을 오름차순으로 정렬합니다. 빈 배열·한 원소·음수·중복값을 허용하며 원본은 바꾸지 않습니다. NaN은 크다·작다 비교가 모두 거짓이라 숫자 정렬의 기준으로 삼기 어려우므로 길이 1일 때도 거절합니다. 입력 검사는 분할이 시작되기 전에 하며, 길이가 작다고 잘못된 원소 검사를 생략하지 않습니다.

유한한 숫자 배열을 복사해 정렬하며 복사본과 작은 쪽 재귀 공간을 구분한 그림
입력 복사본 O(n)과 호출 스택 O(log n)을 합친 추가 공간은 O(n)입니다. 원본 보기 (새 탭)

원본 보존용 복사본이 O(n), 작은 구간 재귀의 호출 스택이 O(log n)이므로 반환 결과를 포함한 추가 공간은 O(n)입니다. 분할 과정은 인덱스와 임시 교환 값만 사용하며 별도 부분 배열을 만들지 않습니다. 멀리 떨어진 값들을 교환하기 때문에 같은 키의 원래 순서를 보장하는 안정 정렬은 아닙니다. 동률 순서가 필요한 문제에서는 이를 입력 계약에 맞춰 선택해야 합니다.

전체 코드와 직접 실행

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

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

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

javascript-quick-sort-first-pivot.mjs 파일을 만들고 아래 전체 코드를 저장합니다.

function copyFiniteNumbers(values) {
  if (!Array.isArray(values)) throw new TypeError('숫자 배열이 필요합니다.');
  for (let i = 0; i < values.length; i++) {
    if (!Object.hasOwn(values, i) || !Number.isFinite(values[i])) {
      throw new TypeError('빈 칸 없는 유한 숫자 배열이 필요합니다.');
    }
  }
  return values.slice();
}

function quickSort(values) {
  const a = copyFiniteNumbers(values);
  function swap(i, j) {
    [a[i], a[j]] = [a[j], a[i]];
  }
  function partition(left, right) {
    const pivot = a[left];
    let boundary = left;
    for (let i = left + 1; i <= right; i++) {
      if (a[i] < pivot) {
        boundary++;
        swap(boundary, i);
      }
    }
    swap(left, boundary);
    return boundary;
  }
  function sort(left, right) {
    while (left < right) {
      const pivotIndex = partition(left, right);
      if (pivotIndex - left < right - pivotIndex) {
        sort(left, pivotIndex - 1);
        left = pivotIndex + 1;
      } else {
        sort(pivotIndex + 1, right);
        right = pivotIndex - 1;
      }
    }
  }
  sort(0, a.length - 1);
  return a;
}

const input = [3, 1, 4, 2];
console.log(JSON.stringify(quickSort(input)));
console.log(JSON.stringify(input));
console.log(JSON.stringify(quickSort([2, -1, 2, 0])));

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

node javascript-quick-sort-first-pivot.mjs

기대 출력

[1,2,3,4]
[3,1,4,2]
[-1,0,2,2]

비용과 적용 범위

시간: 최선 O(n log n), 평균 O(n log n), 최악 O(n²)

퀵 정렬의 최선·평균·최악 시간과 추가 공간 및 최악 입력 예시
첫 피벗 방식은 입력 순서에 따라 시간이 O(n²)까지 늘어날 수 있습니다. 원본 보기 (새 탭)

공간: O(n), 반환 복사본 제외 호출 스택 O(log n)

분석 조건: 첫 원소 피벗과 경계 인덱스로 작은 값 구간을 모으는 분할입니다. 평균은 서로 다른 키가 임의 순서인 가정이며, 정렬된 입력·역순·모두 같은 값에서 최악이 가능합니다. 작은 쪽만 재귀로 처리하여 최악 시간과 별개로 스택 깊이를 제한합니다.

직접 확인해 보기

[2, 3, 1]에서 첫 피벗 2로 한 번 분할하면 피벗은 어느 인덱스에 놓이나요? 양쪽 구간에는 무엇이 남나요?

입력 [2,3,1]의 첫 피벗 2와 미완성 배열을 제시한 자가 확인 문제
첫 피벗의 위치와 양쪽에 남을 값을 직접 생각해 보세요. 원본 보기 (새 탭)
답과 이유 확인

1을 작은 구간으로 옮긴 뒤 피벗과 바꾸므로 [1, 2, 3]이 되고 피벗은 인덱스 1에 놓입니다. 왼쪽 [1]과 오른쪽 [3]은 각각 한 원소여서 바로 끝납니다.

참고 자료

  • Princeton Algorithms — Quicksort — 피벗 분할, 평균과 최악 시간, 불균형 분할 및 중복 키의 영향을 참고했습니다. 예제는 첫 피벗의 경계 스캔과 작은 구간 재귀 방식으로 직접 구성했습니다.

이 글이 도움이 되었나요?

조회 중

이어서 공부하기

체크포인트: 예제의 상태 변화를 손으로 추적하고 아래 문제에 적용하세요. 값이 없는 경우와 원소 하나인 경우도 설명해 보세요. 중복 값과 이미 정렬된 입력을 확인하고 추가 공간과 최악 시간의 조건을 설명하세요.

다음 추천 · 문제: 코딩테스트 투 포인터 풀이: 두 정렬 배열 합치기

다음 주제는 구간과 탐색입니다. 정렬 조건·창의 상태·탐색 구간을 근거로 반복 계산을 줄입니다.

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

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

읽음 기록

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

이 글을 쓴 해비

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

새 글 받아보기

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

RSS 피드 구독하기

댓글 남기기