edubun English

정렬 2: 병합 정렬과 퀵 정렬

값 100만 개를 모든 쌍을 비교해서 정렬하면 비교가 약 5,000억 번 필요하지만, 병합 정렬은 약 2,000만 번이면 됩니다.

문제를 쪼개고 반쪽을 풀기

앞 단원의 방법들은 목록 전체를 한꺼번에 다루고, 그 대가로 약 n²번 비교합니다. 이 단원의 두 방법은 분할 정복이라는 다른 계획을 씁니다. 목록을 작은 부분으로 나누고, 각 부분을 같은 방법으로 정렬한 다음, 정렬된 부분을 합칩니다. 나누기는 부분에 값이 하나이거나 없을 때까지 이어지고, 값이 하나뿐인 목록은 이미 정렬된 것입니다. 이 방법은 더 작은 목록에 자기 자신을 다시 쓰므로 재귀적입니다(재귀는 6단원에서 자세히 다룹니다).

병합 정렬: 일은 합치는 데서 합니다

병합 정렬은 목록을 반으로 자르고, 왼쪽 절반을 정렬하고, 오른쪽 절반을 정렬한 뒤, 정렬된 두 절반을 하나로 합칩니다. 재치 있는 부분은 합치기입니다. 정렬된 두 목록은 맨 앞 값만 보고도 합칠 수 있습니다. 두 맨 앞 값을 비교해 작은 값을 적고, 그 값을 지나갑니다. 한쪽이 빌 때까지 되풀이하고, 남은 쪽은 그대로 이어 붙입니다. [1, 4, 7]과 [2, 3, 8]을 합치면 1(1과 2), 2(4와 2), 3(4와 3), 4(4와 8), 7(7과 8) 순서로 적고 마지막 8을 붙입니다. 값 6개에 비교 5번입니다. 값이 모두 n개인 두 목록을 합칠 때 비교는 n − 1번을 넘지 않습니다.

function merge(left, right) {
  const out = [];
  let i = 0, j = 0;
  while (i < left.length && j < right.length) {
    if (right[j] < left[i]) out.push(right[j++]);
    else out.push(left[i++]);
  }
  return out.concat(left.slice(i), right.slice(j));
}

function mergeSort(a) {
  if (a.length <= 1) return a;          // already sorted
  const mid = Math.floor(a.length / 2);
  return merge(mergeSort(a.slice(0, mid)), mergeSort(a.slice(mid)));
}

왜 빠를까요? 반으로 나누는 과정을 층으로 그려 봅니다. 값 8개는 4개짜리 2개로, 다시 2개짜리 4개로, 다시 1개짜리 8개로 나뉩니다. 8은 세 번 반으로 가를 수 있으니 층은 셋입니다. 한 층에서 이루어지는 합치기를 모두 더하면 값 하나하나를 한 번씩 다루므로, 한 층에 드는 비교는 많아야 약 n번입니다. 층이 log₂ n개이므로 모두 약 n log₂ n번입니다. 값 100만 개라면 약 2,000만 번으로, 쌍마다 비교하는 방법의 약 5,000억 번과 비교됩니다. [38, 27, 43, 3, 9, 82, 10]을 실험실처럼 [38, 27, 43, 3]과 [9, 82, 10]으로 나누면 비교 14번이면 되고, 위 코드처럼 3개와 4개로 나누면 13번입니다.

대가는 메모리입니다. 합치려면 결과를 적을 곳이 있어야 하므로 병합 정렬은 입력만 한 목록을 하나 더 씁니다. 병합 정렬은 안정적이기도 합니다. 맨 앞 값이 같을 때 왼쪽 목록에서 먼저 가져오므로, 같다고 비교되는 값들이 원래 순서를 지킵니다. 표를 도시로 정렬한 다음 이름으로 정렬하는 것처럼 열을 바꿔 가며 정렬할 때 이 성질이 중요합니다.

불러오는 중

끌어서 돌리고, 휠이나 두 손가락으로 확대하세요.

병합 정렬은 합치는 구간을 색칠하고, 퀵 정렬은 피벗을 보라색으로 표시합니다. 개수를 40으로 올려 삽입 정렬과 숫자를 비교해 보세요.

퀵 정렬: 피벗을 기준으로 나누기

퀵 정렬은 다른 방식으로 나눕니다. 값 하나를 골라 피벗으로 삼고, 피벗보다 작은 값은 모두 앞으로, 나머지는 뒤로 오도록 목록을 다시 늘어놓습니다. 이 단계를 분할이라고 합니다. 이제 피벗은 최종 자리에 있고, 양쪽은 같은 방법으로 따로 정렬하면 됩니다. 왼쪽 값은 모두 오른쪽 값보다 작으므로 나중에 합칠 일이 없습니다.

실험실은 각 부분의 마지막 값을 피벗으로 쓰는 흔한 교과서 방식을 씁니다. [5, 1, 4, 2, 8]에서 피벗 8을 나머지 네 값과 비교하면 모두 작으므로 8은 끝에 그대로 있습니다. 다음 부분 [5, 1, 4, 2]는 피벗 2를 씁니다. 1만 더 작으므로 교환 두 번 뒤 목록은 [1, 2, 4, 5, 8]이 됩니다. 비교 한 번으로 [4, 5]를 처리하면 끝입니다. 모두 비교 8번, 교환 2번입니다.

피벗이 가운데 근처에 떨어지면 나눌 때마다 문제가 반으로 줄어 퀵 정렬도 병합 정렬처럼 약 n log₂ n번 비교합니다. 게다가 복사본 없이 원래 목록 안에서 일합니다. 하지만 피벗을 잘못 고르면 손해를 봅니다. 이미 정렬된 목록에서 늘 마지막 값을 피벗으로 쓰면 나눌 때마다 값이 하나씩만 떨어져 나가고, 퀵 정렬은 약 n²/2번 비교로 돌아갑니다. [1, 2, 3, 4, 5]라면 선택 정렬과 같은 10번입니다. 실제 라이브러리는 피벗을 무작위로 고르거나 표본 세 값의 가운데 값을 쓰는 식으로 더 조심해서 고릅니다.

배경두 방법은 기원이 알려져 있습니다. 병합 정렬은 첫 프로그램 내장식 컴퓨터를 설계하던 1945년에 존 폰 노이만이 제안했습니다. 퀵 정렬은 1959년 모스크바 국립대학교에서 공부하던 영국인 학생 토니 호어에게서 나왔습니다. 그는 러시아어를 영어로 옮기는 기계 번역 일을 제안받았는데, 사전이 자기 테이프에 들어 있어서 문장의 낱말을 먼저 알파벳순으로 늘어놓아야 했습니다. 처음 떠올린 방법은 문장 길이의 제곱에 비례하는 시간이 걸렸고, 두 번째 방법이 퀵 정렬이 되었습니다. 그는 이것을 1961년에 발표했습니다.
방법비교 횟수추가 메모리
삽입 정렬보통 약 n²/4, 최악 약 n²/2없음. 안정적
병합 정렬언제나 약 n log₂ n값 n개짜리 목록 하나. 안정적
퀵 정렬보통 약 n log₂ n, 최악 약 n²/2(피벗이 나쁠 때)적음. 흔한 방식은 안정적이지 않음
비교로 정렬하는 방법이 n log₂ n보다 나을 수 있을까요? 없습니다. 쌍을 비교해서만 정보를 얻는 정렬은 가능한 처음 순서 n!가지를 모두 구별할 수 있어야 하고, 비교 한 번의 결과는 두 가지뿐입니다. 그래서 최악의 경우 적어도 log₂(n!)번 비교해야 하고, 이 값은 n log₂ n처럼 늘어납니다. 값 10개라면 이 하한은 22번입니다.
분할 정복문제를 나누고 같은 방법으로 부분을 푼 뒤 결과를 합침
합치기(병합)맨 앞 값 중 작은 것을 거듭 가져와 정렬된 두 목록을 합침
병합 정렬반으로 나누고 각각 정렬해 합침. 비교 약 n log₂ n번
피벗퀵 정렬이 목록을 나누는 기준 값
분할작은 값은 피벗 앞, 나머지는 뒤로 오게 다시 놓음
퀵 정렬피벗으로 분할한 뒤 양쪽을 정렬
안정 정렬같은 값끼리 원래 순서를 지킴
제자리 정렬추가 메모리를 거의 쓰지 않고 원래 목록 안에서 정렬함
  1. [1, 4, 7]과 [2, 3, 8]을 합칠 때 비교는 몇 번일까요?
  2. [1, 2, 3]과 [4, 5, 6]을 합칠 때 비교는 몇 번일까요?
  3. 값 16개를 하나씩이 될 때까지 반으로 나누면 몇 층이 될까요?
  4. 병합 정렬은 값 100만 개에서 대략 몇 번 비교할까요?
    • 약 20번
    • 약 2,000만 번
    • 약 5,000억 번
    • 정확히 100만 번
  5. 분할이 끝나면 피벗은 어디 있을까요?
    • 정렬이 끝났을 때의 최종 자리
    • 맨 앞
    • 맨 끝
    • 어디든, 나중에 또 움직임
  6. 마지막 값을 피벗으로 쓰는 퀵 정렬이 이미 정렬된 [1, 2, 3, 4, 5]를 정렬합니다. 비교는 몇 번일까요?
  7. 입력만 한 목록을 하나 더 쓰는 방법은?
    • 병합 정렬
    • 퀵 정렬
    • 삽입 정렬
  8. 1945년에 병합 정렬을 제안한 사람은?
    • 토니 호어
    • 존 폰 노이만
    • 에이다 러브레이스
    • 유클리드
  9. 토니 호어가 퀵 정렬을 떠올린 계기는?
    • 자기 테이프에 담긴 사전을 찾으려고 문장 낱말을 정렬하는 일
    • 도서관 책 정렬
    • 웹 페이지 순위 매기기
    • 은행 수표 정렬
  10. 병합 정렬을 안정 정렬이라고 하는 이유는?
    • 같은 값이 원래 순서를 지켜서
    • 절대 멈추지 않아서
    • 늘 같은 시간이 걸려서
    • 추가 메모리를 쓰지 않아서
  11. 값 10개를 비교로 정렬하는 어떤 방법이든 최악의 경우 적어도 몇 번 비교해야 할까요(log₂ 10!을 올림)?
핵심 정리
  • 분할 정복: 나누고, 부분을 풀고, 합칩니다.
  • 병합 정렬: 늘 약 n log₂ n번 비교, 추가 메모리 필요, 안정적입니다.
  • 퀵 정렬: 피벗으로 분할합니다. 평균은 빠르지만 피벗이 나쁘면 약 n²/2번입니다.
  • 값 100만 개라면 5,000억 번 대신 약 2,000만 번입니다.
  • 비교 정렬은 최악의 경우 약 n log₂ n보다 나을 수 없습니다.