quick sort — 최악 O(n²)인데 평균은 왜 O(n log n)인가

quick sort는 최악의 경우 O(n2)O(n^2)까지 느려진다. 그런데도 현장에서 가장 널리 쓰이는 정렬이다. 모순처럼 보이는 이 사실의 답은 “평균”에 있다. 그 평균이 정말 O(nlogn)O(n \log n)인지, 기댓값 점화식을 세워 끝까지 풀어 본다.

이 포스트에서 다루는 내용
  • 두 포인터 ii, jj한 배열 안에서 분할하는 과정
  • 배열을 2개 쓰는 더 단순한 분할과 그 비용
  • pivot이 어디에 놓이느냐로 갈리는 최선 O(nlogn)O(n \log n) · 최악 O(n2)O(n^2)
  • 평균 분석의 핵심. 기댓값 점화식 E(n)E(n)을 세워 직접 풀어 Θ(nlogn)\Theta(n \log n)을 유도

정렬 알고리즘 포스트에서 quick sort의 분할·올바름 증명·복잡도의 개요를 다뤘다. 그 글은 “평균은 왜 O(nlogn)O(n \log n)인가”를 “한쪽으로 너무 쏠리지만 않으면 깊이가 로그” 라는 직관으로만 짚고 넘어갔다. 이 글은 그 빈칸을 채운다. 평균이 Θ(nlogn)\Theta(n \log n)임을 수식으로 유도하는 것이 목표다.


분할 정복이지만, 절반을 보장하지 않는다

quick sort는 분할 정복이다. 하지만 merge sort가 배열을 정확히 절반으로 자르는 것과 달리, quick sort는 pivot이라 부르는 한 원소를 기준으로 “작은 것은 왼쪽, 큰 것은 오른쪽”으로 나눈다. 이 분할이 절반에 가까울지, 한쪽으로 쏠릴지는 입력과 pivot 선택에 달려 있다. 바로 이 점이 quick sort의 복잡도가 입력마다 달라지는 이유이고, 평균을 따져야만 하는 이유다.

먼저 분할이 어떻게 일어나는지부터 본다.

두 포인터로 한 배열 안에서 분할하기

가장 왼쪽 값 a[0]a[0]을 pivot pp로 잡고, 두 포인터로 배열을 한 번 훑으며 제자리에서 분할한다.

void sort(int a[], int n) {
    if (n <= 1) return;     // 기저 사례: 크기 0·1은 이미 정렬됨
    int p = a[0];           // pivot: 맨 왼쪽 값
    int i = 1;              // 왼쪽 → 오른쪽으로 다가가는 포인터
    int j = n - 1;          // 오른쪽 → 왼쪽으로 다가가는 포인터
    while (i <= j) {
        while (i <= j && a[i] < p) i++;   // p보다 작은 동안 전진 → p 이상에서 멈춤
        while (i <= j && a[j] >= p) j--;  // p 이상인 동안 후진 → p 미만에서 멈춤
        if (i < j) swap(a[i], a[j]);      // 엇갈리기 전이면 두 값을 교환
    }
    swap(a[0], a[j]);       // pivot을 경계 j(항상 a[j] < p, 또는 j==0)로 → 최종 위치
    sort(a, j);             // 왼쪽 영역 재귀 (크기 j)
    sort(a + j + 1, n - j - 1);  // 오른쪽 영역 재귀 (크기 n−j−1)
}

과정

  1. 맨 왼쪽의 값을 pp(pivot)으로 잡는다.
  2. 왼쪽에서 오른쪽으로 다가가는 포인터를 ii, 오른쪽에서 왼쪽으로 다가가는 포인터를 jj라 하자.
  3. iipp 이상인 값(pip_i)을 만날 때까지 전진한다. jjpp보다 작은 값(pjp_j)을 만날 때까지 후진한다.
  4. pipp_i \ge p이고 pj<pp_j < p이므로, pip_ipjp_j를 맞바꾸면 둘 다 제 영역으로 들어간다. 그러면 iijj는 다시 전진·후진을 이어 갈 수 있다.
  5. iijj가 서로를 가로질러 엇갈리면 배열은 이렇게 정리되어 있다. 경계의 왼쪽에는 pp보다 작은 값들만, 오른쪽에는 pp 이상인 값들만 남는다. pivot과 같은 값은 오른쪽에 모인다.
  6. 따라서 마지막에 pp(즉 a[0]a[0])를 경계 자리 a[j]a[j]와 바꾸면, pp를 기준으로 왼쪽엔 작은 값, 오른쪽엔 pp 이상인 값만 놓인다. 이 순간 pp의 자리는 정렬이 끝난 뒤의 자리와 같다. 즉 최종 위치다.
  7. 남은 일은 왼쪽 영역과 오른쪽 영역을 각각 재귀로 정렬하는 것뿐이다.

두 내부 조건이 한쪽은 a[i] < p, 다른 쪽은 a[j] >= p비대칭인 점에 주목하자. pivot과 같은 값(=p= p)이 여러 개 있어도, jj가 그런 값을 건너뛰며 후진하므로 포인터가 항상 한 칸 이상 진행한다. 덕분에 [5, 5]처럼 같은 값만 있는 입력에서도 멈추지 않고 정확히 정렬된다. 다만 정확히 정렬된다는 것과 빠르다는 것은 다른 이야기다. 같은 값만 있는 입력의 비용은 아래 평균 분석에서 다시 본다.

quick sort 분할 과정 — 두 포인터 i와 j가 양 끝에서 안쪽으로 다가오며, 엇나간 값들을 맞바꾸고 엇갈리는 지점에서 pivot이 경계로 들어간다
quick sort 분할 과정 — 두 포인터 i와 j가 양 끝에서 안쪽으로 다가오며, 엇나간 값들을 맞바꾸고 엇갈리는 지점에서 pivot이 경계로 들어간다

정렬 알고리즘 포스트의 분할은 경계 인덱스 하나(s)로 작은 값을 앞으로 모으는 방식이었다. 여기 두 포인터 방식은 같은 결과(작은 값 왼쪽 · pp 이상 오른쪽 · pivot은 최종 위치)를 양 끝에서 좁혀 오며 만든다. 분할의 결과는 같고 경로가 다를 뿐이다.

배열 2개를 쓰는 더 단순한 방법

위 방법은 배열 하나 안에서 끝내려다 보니 두 포인터를 엇갈리게 다루는 과정이 다소 복잡해 보인다. 배열을 두 개 쓰면 ii, jj를 둘 필요가 없다.

원본을 앞에서부터 훑으며, pp보다 작은 값은 새 배열의 왼쪽 끝부터 차곡차곡 채우고, 큰 값은 오른쪽 끝부터 채운다. 다 훑고 나면 가운데 빈 한 칸에 pp를 넣으면 된다. 포인터를 엇갈리게 관리할 필요 없이, 한 번의 패스로 분할이 끝난다.

대신 길이 nn짜리 배열을 하나 더 쓰므로 추가 메모리 O(n)O(n) 이 든다. 이는 merge sort가 떠안는 메모리 비용과 같은 종류의 트레이드오프다. quick sort가 실측에서 빠른 큰 이유 중 하나가 제자리(in-place) 분할이라는 점을 생각하면, 두 배열 방식은 이해를 돕는 설명용에 가깝다.


시간 문제: pivot이 어디에 놓이는가

분할 자체는 배열을 한 번 훑으므로 O(n)O(n)이다. 전체 복잡도는 pivot이 분할 후 어디에 놓이느냐, 즉 양쪽 영역의 크기가 어떻게 갈리느냐에 전적으로 달려 있다.

  • 정확히 반으로 나뉠 때(최선). pivot이 매번 중앙값이면 T(n)=2T(n/2)+nT(n) = 2T(n/2) + n이 되어, merge sort와 같은 O(nlogn)O(n \log n)이다.
  • 한쪽으로 완전히 쏠릴 때(최악). pivot이 매번 최솟값이나 최댓값이면 한쪽은 00개, 다른 쪽은 n1n-1개가 된다. T(n)=T(n1)+nT(n) = T(n-1) + n이 되어 O(n2)O(n^2)로 추락한다. 이미 정렬된(또는 역순으로 정렬된) 배열에 항상 a[0]a[0]을 pivot으로 잡으면 정확히 이 최악 경우다.

O(nlogn)O(n \log n)O(n2)O(n^2)의 간극은 거대하다. 그렇다면 “보통” 입력에서는 어느 쪽에 가까울까? 이 질문에 답하려면 평균을 정의해야 한다.


평균은 왜 O(nlogn)O(n \log n)인가

평균을 말하려면 확률을 가정해야 한다

“평균”은 입력의 확률분포가 주어져야만 말할 수 있는 개념이다. 분포가 없으면 평균도 없다. 아래 분석이 세우는 모형은 이것이다.

분석 모형

입력은 서로 다른 nn개의 키이고, 그 배열 순서는 n!n!가지 순열 중 하나가 균등한 확률로 뽑힌 것이다.

이 모형에서 pivot인 a[0]a[0]이 정렬 후 몇 등인지는 11등부터 nn등까지 모두 같은 확률 1n\frac{1}{n}이다. 그리고 분할이 끝난 뒤 왼쪽 k1k-1개와 오른쪽 nkn-k개의 내부 순서도 다시 균등 무작위 순열이므로, 부분문제의 비용에 E(k1)E(k-1)E(nk)E(n-k)를 그대로 넣을 수 있다. 점화식이 성립하는 것은 이 두 사실 덕분이다.

입력 분포를 가정하고 싶지 않다면 뒤집어도 된다. pivot을 배열의 첫 원소가 아니라 균등 무작위로 고르면 입력이 무엇이든 등수가 1n\frac{1}{n}로 흩어진다. 아래 점화식은 두 모형 어느 쪽에서도 같은 모양으로 선다.

분석의 무기는 기댓값의 선형성이다. 어떤 두 양 AA, BB에 대해서도 다음이 성립한다.

E(A+B)=E(A)+E(B)E(A + B) = E(A) + E(B)

이 성질 덕분에, 복잡하게 얽힌 비용도 조각조각 나눠 기댓값을 더할 수 있다.

기댓값 점화식 세우기

nn개를 정렬하는 데 드는 시간의 기댓값을 E(n)E(n)이라 하자. pivot의 ‘등수’를 kk라 하면, 분할 후 왼쪽에는 k1k-1개, 오른쪽에는 nkn-k개가 남는다.

pivot 등수 kk왼쪽 크기오른쪽 크기
1100n1n-1
2211n2n-2
3322n3n-3
\vdots\vdots\vdots
nnn1n-100
평균 분석의 분배 구조 — pivot 등수 k가 1/n 확률로 정해지고, 그에 따라 왼쪽 k−1개와 오른쪽 n−k개로 갈린 두 부분문제의 기댓값이 더해진다
평균 분석의 분배 구조 — pivot 등수 k가 1/n 확률로 정해지고, 그에 따라 왼쪽 k−1개와 오른쪽 n−k개로 갈린 두 부분문제의 기댓값이 더해진다

각 등수 kk가 확률 1n\frac{1}{n}로 나오고, 그때 두 부분문제의 비용은 E(k1)E(k-1)E(nk)E(n-k)다. 여기에 분할 자체의 비용 nn(ii, jj가 배열 전체를 훑는 시간)을 더하면, 기댓값의 선형성으로 다음 점화식이 선다.

E(n)=k=1nE(k1)+E(nk)n+nE(n) = \sum_{k=1}^n \frac{E(k-1) + E(n-k)}{n} + n

점화식을 풀어 나가기

합을 펼쳐 쓰면, kk11에서 nn까지 돌 때 E(0),E(1),,E(n1)E(0), E(1), \ldots, E(n-1)이 각각 두 번씩 나타난다.

E(n)={E(0)+E(n1)}+{E(1)+E(n2)}++{E(n1)+E(0)}n+nE(n) = \frac{\{E(0) + E(n-1)\} + \{E(1) + E(n-2)\} + \cdots + \{E(n-1) + E(0)\}}{n} + n

같은 항이 두 번씩이므로 묶으면,

E(n)=2nk=0n1E(k)+nE(n) = \frac{2}{n} \sum_{k=0}^{n-1} E(k) + n

양변에 nn을 곱해 합 기호를 다루기 쉽게 만든다.

nE(n)=2k=0n1E(k)+n2n \cdot E(n) = 2\sum_{k=0}^{n-1} E(k) + n^2

이제 같은 식을 nn 대신 n1n-1에 대해 써 보자.

(n1)E(n1)=2k=0n2E(k)+(n1)2(n-1)\cdot E(n-1) = 2\sum_{k=0}^{n-2} E(k) + (n-1)^2

두 식을 빼면 합 기호가 E(n1)E(n-1) 한 항만 남기고 사라진다.

nE(n)(n1)E(n1)=2E(n1)+n2(n1)2=2E(n1)+2n1n E(n) - (n-1)E(n-1) = 2E(n-1) + n^2 - (n-1)^2 = 2E(n-1) + 2n - 1

(n1)E(n1)(n-1)E(n-1)을 우변으로 넘겨 정리하면, 깔끔한 1차 점화식이 된다.

nE(n)=(n+1)E(n1)+2n1n \cdot E(n) = (n+1)\cdot E(n-1) + 2n - 1

텔레스코핑으로 닫힌 식에 다가가기

양변을 n(n+1)n(n+1)로 나누면, E(n)n+1\frac{E(n)}{n+1}E(n1)n\frac{E(n-1)}{n}이 같은 꼴로 나란히 놓인다.

E(n)n+1=E(n1)n+2n1n(n+1)=E(n1)n+3n+11n\frac{E(n)}{n+1} = \frac{E(n-1)}{n} + \frac{2n - 1}{n(n+1)} = \frac{E(n-1)}{n} + \frac{3}{n+1} - \frac{1}{n}

마지막 등호는 2n1n(n+1)\frac{2n-1}{n(n+1)}을 부분분수로 쪼갠 것이다(1n+3n+1-\frac{1}{n} + \frac{3}{n+1}). 이제 F(n)=E(n)n+1F(n) = \dfrac{E(n)}{n+1}로 두면 식은 한 칸씩 더해 나가는 모양이 된다.

F(n)=F(n1)+(3n+11n)F(n) = F(n-1) + \left(\frac{3}{n+1} - \frac{1}{n}\right)

F(n1)F(n-1)을 다시 F(n2)F(n-2)로, 그 다음을 또 그 앞으로 바꾸어 바닥까지 펼치면(텔레스코핑) 추가항들이 전부 더해진다.

3211+3312+3413+3514+\frac{3}{2} - \frac{1}{1} + \frac{3}{3} - \frac{1}{2} + \frac{3}{4} - \frac{1}{3} + \frac{3}{5} - \frac{1}{4} + \cdots

같은 분모끼리 다시 묶으면 정리된다(유한합이라 실제로는 꼬리항 3n+1\frac{3}{n+1}과 초기값 F(0)F(0)가 남지만, 둘 다 상수·저차항이라 점근 분석에서 무시한다. 그래서 아래 등호는 지배항만 남긴 근사다).

=11+(3212)+(3313)+(3414)+=3+2(11+12+13+14+)= -\frac{1}{1} + \left(\frac{3}{2} - \frac{1}{2}\right) + \left(\frac{3}{3} - \frac{1}{3}\right) + \left(\frac{3}{4} - \frac{1}{4}\right) + \cdots = -3 + 2\left(\frac{1}{1} + \frac{1}{2} + \frac{1}{3} + \frac{1}{4} + \cdots\right)

여기서 괄호 안은 조화수(harmonic number) Hn=k=1n1kH_n = \sum_{k=1}^{n} \frac{1}{k}이고, 잘 알려진 대로 HnlnnH_n \approx \ln n이다(자연로그가 나오는 이유는 1k\frac{1}{k}의 합이 1xdx=lnx\int \frac{1}{x}\,dx = \ln x에 대응하기 때문이다). 따라서 추가항들의 총합은 약 2lnn32\ln n - 3이고, 텔레스코핑의 결과로 F(n)F(n) 자체가 이 값에 닿는다.

E(n)n+12lnn3\frac{E(n)}{n+1} \approx 2\ln n - 3
한 줄 주의

이 마지막 식은 누적된 총합이다. 한 칸짜리 점화식 F(n)=F(n1)+()F(n) = F(n-1) + (\cdots)과 혼동해 우변에 E(n1)n\frac{E(n-1)}{n}을 그대로 남겨 두면 같은 양을 두 번 세게 된다. 텔레스코핑은 그 E(n1)n\frac{E(n-1)}{n}을 끝까지 펼쳐 위의 급수로 대체하는 과정이다.

결론: 평균은 Θ(nlogn)\Theta(n \log n)

E(n)n+12lnn\frac{E(n)}{n+1} \approx 2\ln n이므로 양변에 (n+1)(n+1)을 곱하면 E(n)2(n+1)lnnE(n) \approx 2(n+1)\ln n이다. 자연로그 lnn\ln n과 임의 밑의 logn\log n은 상수배만큼만 다르므로(lnn=log2nln2\ln n = \log_2 n \cdot \ln 2), 점근적으로는 다음과 같이 쓴다.

E(n)2(n+1)lnn=Θ(nlogn)E(n) \approx 2(n+1)\ln n = \Theta(n \log n)
중복 키를 허용하면 이 분석이 왜 무너지는가

앞 절의 분할은 중복 키가 있어도 올바르게 동작한다. 하지만 위 모형은 키가 서로 다르다고 두었고, 그 가정이 빠지면 등수가 1n\frac{1}{n}로 흩어진다는 전제부터 성립하지 않는다.

극단을 보면 분명하다. 모든 원소가 같은 값인 배열에서는 ii가 첫 칸에서 곧바로 멈추고 jj만 끝까지 내려온다. 경계가 항상 j=0j = 0이 되어 왼쪽 00개, 오른쪽 n1n-1개로 갈린다. 매 단계가 최악의 쏠린 분할이므로 비교 횟수는 정확히 (n2)\binom{n}{2}다. n=400n = 400이면 79,800번으로, 같은 크기의 서로 다른 무작위 입력에서 관측되는 3,700번 남짓과 스무 배 넘게 벌어진다.

이 입력은 위 모형의 표본 공간에 들어 있지 않으므로 Θ(nlogn)\Theta(n \log n)이라는 결론과 모순되지 않는다. 실무에서 중복이 많은 데이터를 다룰 때 세 갈래 분할(작다·같다·크다)을 쓰는 이유가 여기에 있다. 같은 값을 가운데로 몰아 재귀에서 아예 빼면 이 최악이 사라진다.

최악이 O(n2)O(n^2)인 알고리즘의 평균O(nlogn)O(n \log n)이라는 결론이다. 직관으로도 들어맞는다. O(nlogn)O(n \log n)O(n2)O(n^2)의 간극이 워낙 크기 때문에, 최악의 쏠린 분할이 매 단계 반복되어야만 평균이 O(n2)O(n^2) 쪽으로 끌려간다. 그런데 무작위 입력에서 그런 극단이 매번 일어날 확률은 지극히 작아, 기대 깊이는 로그 규모에 머문다. 실험적으로 측정해 봐도 평균은 O(nlogn)O(n \log n)으로 나타난다.

재귀 깊이 비교 — 균형 분할은 깊이가 약 log n으로 얕고, 한쪽으로 쏠린 분할은 깊이가 n까지 깊어진다
재귀 깊이 비교 — 균형 분할은 깊이가 약 log n으로 얕고, 한쪽으로 쏠린 분할은 깊이가 n까지 깊어진다
핵심 정리
  • quick sort는 pivot 하나로 “작은 값 왼쪽 · pp 이상 오른쪽”으로 가르는 분할 정복이다. 분할은 O(n)O(n)이고, 두 포인터로 제자리에서 끝낼 수 있다.
  • 복잡도는 pivot의 위치로 갈린다. 절반에 가까우면 최선 O(nlogn)O(n \log n), 한쪽으로 완전히 쏠리면 최악 O(n2)O(n^2).
  • 평균 분석: 서로 다른 nn개 키의 균등 무작위 순열(또는 무작위 pivot)을 두면 모든 등수가 확률 1n\frac{1}{n}로 같아져 E(n)=2nk=0n1E(k)+nE(n) = \frac{2}{n}\sum_{k=0}^{n-1}E(k) + n. 이를 1차 점화식 nE(n)=(n+1)E(n1)+2n1nE(n) = (n+1)E(n-1) + 2n-1로 바꾸고, E(n)n+1\frac{E(n)}{n+1}을 텔레스코핑하면 조화수 HnlnnH_n \approx \ln n이 나타나 E(n)2(n+1)lnn=Θ(nlogn)E(n) \approx 2(n+1)\ln n = \Theta(n \log n).
  • 결국 quick sort는 최악을 피할 수만 있다면(무작위 pivot 등) 평균적으로 최적에 가까운 정렬이며, 제자리 분할의 캐시 이점까지 더해 실측에서 자주 가장 빠르다.
유도에 관한 한마디

평균 복잡도의 정확한 유도는 조화수의 점근, 부분분수, 텔레스코핑이 한데 얽혀 까다롭다. 위 전개는 상수항을 근사로 흘려보낸 곳이 있지만, 결론인 Θ(nlogn)\Theta(n \log n)은 변하지 않는다. 더 엄밀한 분석은 무작위 pivot을 쓰는 무작위화 quick sort의 기대 시간 분석에서 같은 결론에 더 단단하게 도달한다.

이어지는 글

quick sort의 분할과 올바름 증명, 다른 정렬과의 비교는 정렬 알고리즘 포스트에 정리되어 있다. 정확히 절반으로 나누는 사촌 격인 merge sort와, 비교 기반 정렬이 Ω(nlogn)\Omega(n \log n)보다 빠를 수 없다는 하한 증명은 분할 정복 포스트에서 다룬다. 점화식을 푸는 일반 도구가 궁금하다면 마스터 정리를 함께 보면 좋다.

© 2026 XsQuare01. Powered by GitHub Pages. · 방문자