
들어가기 앞서서...
우리는 컴퓨터 공학을 배우면서 알고리즘의 성능을 분석하기 위해 Big-O 표기법을 접하게 된다.
대부분 컴퓨터 공학자들은 지루한 사람들이므로 기억이 안 나는게 당연하고 또 대강 이해하면서 넘어 간다.
일반적으로는
“O(1)에 가까울수록 빠르고, O(n!)에 가까울수록 느리다”는 식으로 단순하게 이해하는 경우가 많다.
그렇다면 Big-O의 정확한 정의는 무엇이며, 왜 이러한 표기법이 사용되는 것일까?

(빅 O 표기법)
빅 O 표기법의 시작
빅 O 표기법은 1894년 독일 수학자 파울 바흐만(Paul Bachmann)이 그의 저서 《Analytische Zahlentheorie》(해석적 정수론)에서 처음 소개했고
바흐만은 함수의 성장률을 비교하고 근사적으로 표현하기 위해 이 표기법을 도입했다.
이후 에드문트 란다우(Edmund Landau)가 이 개념을 더욱 정교화하고 확산시켰는데
란다우는 빅 O 표기법을 "란다우 표기법(Landau notation)"이라고도 불리었고, 컴퓨터 과학에서 알고리즘의 복잡도를 계산하기 위해
단순하지만 강력한 도구로 사용되었다.
이외에도 빅 O 표기법은 하한을 표현하는 빅 오메가, 상한과 하한을 동시에 표현하는 빅 세타등으로 변형되기도 했다.

(워싱턴대학 CSE 373 슬라이드)
함수 과 이 있을 때, 은 충분히 큰 에 대해
을 만족하는 양의 상수 가 존재함을 의미한다.
즉, 은 의 상수 배로 상한(upper bound)이 정해질 수 있다는 뜻이다.
예시를 하나 들어보자.
f(n) = 3n² + 2n + 1
일 때, 은 이다. 최고차항인 만 고려하고, 상수 계수는 무시하기 때문이다.
그렇다면 일 때는?
이 경우 이다.
빅 O 표기법은 함수의 점근적 성장률(asymptotic growth rate)을 표현하는 도구다.
한 문장으로 요약하면, 입력 크기가 커질수록 알고리즘의 복잡도가 어떻게 증가하는지를 나타낸다.
Big-O를 이용해 보통 두 가지 복잡도를 분석한다.

시간 복잡도(Time Complexity)
- 입력 크기에 따라서 알고리즘이 수행되는데 걸리는 '시간(연산횟수)'의 증가율을 나타낸다.
대표적인 Big O 시간 복잡도
O(1) → 상수 시간 (Constant Time)
예시:
배열에서 인덱스로 요소 접근 (예: arr[2]).
해시 테이블에서 키를 이용한 값 조회 (충돌 없는 경우).
설명: 입력 크기(n)와 무관하게 연산이 한 단계로 완료됨
O(log n) → 로그 시간 (Logarithmic Time)
예시: 이진 탐색으로 정렬된 배열에서 값 찾기.
균형 이진 탐색 트리(BST)에서 노드 검색.
설명: 입력 크기가 커질 때마다 연산 범위가 절반으로 줄어듬 (예: 데이터를 계속 분할하며 탐색).
O(n) → 선형 시간 (Linear Time)
예시: 배열의 모든 요소를 한 번씩 순회하는 반복문.
연결 리스트(Linked List)에서 특정 값 탐색.
설명: 입력 크기(n)에 정비례해 연산 시간이 증가
O(n log n) → 로그-선형 시간 (Linearithmic Time)
예시: 병합 정렬(Merge Sort), 퀵 정렬(Quick Sort), 힙 정렬(Heap Sort).
대부분의 효율적인 비교 기반 정렬 알고리즘.
설명: 분할 정복(Divide and Conquer) 전략을 사용하며, n개의 요소를 반복적으로 절반으로 나누고(log n), 병합(n)
O(n²) → 이차 시간 (Quadratic Time)
예시: 중첩 반복문으로 배열의 모든 조합 확인 (예, 버블 정렬, 선택 정렬).
그래프에서 모든 노드 쌍의 최단 경로 계산 (플로이드-워셜 알고리즘).
설명: 입력 크기가 커질수록 연산 시간이 제곱으로 폭발적 증가.
O(2ⁿ) → 지수 시간 (Exponential Time)
예시: 피보나치 수열의 나이브 재귀 구현 (중복 계산 많음).
모든 가능한 부분집합(Subset) 생성.
설명: 입력 크기가 작아도 연산 시간이 기하급수적으로 증가하여 실용적이지 않다.
공간 복잡도(Space Complexity)
-입력 크기에 따라 알고리즘이 사용하는 메모리 양의 증가율을 나타낸다.
대표적인 Big O 공간 복잡도
O(1) → 상수 공간 (Constant Space)
예시: 단일 변수 사용 (예: 반복문의 인덱스 i).
입력 크기와 무관한 고정된 크기의 변수 (e.g., int a = 10).
설명: 입력 크기(n)와 관계없이 고정된 메모리만 사용
O(log n) → 로그 공간 (Logarithmic Space)
예시:균형 이진 탐색 트리(BST)의 재귀적 순회 (재귀 호출 스택 깊이).
퀵 정렬의 평균 공간 복잡도 (분할 정복 시 스택 깊이).
설명: 재귀 또는 분할 정복 알고리즘에서 스택/메모리 사용량이 입력 크기의 로그에 비례.
O(n) → 선형 공간 (Linear Space)
예시: 입력 배열을 복사해 저장 (예: newArr = arr.slice()).
그래프의 인접 리스트 표현 (노드 수에 비례한 메모리).
설명:입력 크기(n)에 정비례해 메모리 사용량이 증가
O(n log n) → 로그-선형 공간 (Linearithmic Space)
예시:병합 정렬(Merge Sort)의 최악 공간 복잡도 (임시 배열 사용).
특정 분할 정복 알고리즘의 재귀적 구현 (단계별 추가 메모리).
설명:재귀 호출 단계마다 선형 공간을 사용하고, 재귀 깊이가 로그인 경우 발생
O(n²) → 이차 공간 (Quadratic Space)
예시:그래프의 인접 행렬 표현 (n x n 행렬).
동적 계획법(DP)**의 2차원 테이블 (예: 최장 공통 부분 수열).
설명:입력 크기의 제곱에 비례해 메모리가 증가하며, 대규모 데이터에 비효율적
O(2ⁿ) → 지수 공간 (Exponential Space)
예시: 모든 부분집합(Subset) 저장 (예: n개 요소의 모든 조합).
재귀적 피보나치의 최악 공간 복잡도 (중복 호출 스택²).
설명:입력 크기가 작아도 메모리 사용량이 기하급수적으로 증가
둘 다 빅 오 표기법으로 표현되며, Big-O는 상한(upper bound)을 표현하는 표기이지만, 자주 Worst Case 분석과 함께 사용되는 경우가 많다.
차이점이 있다면
-시간 복잡도는 속도에 초점을 두고, 공간 복잡도는 메모리 사용량에 초점을 둔다.
트레이드 오프(trade-off)
-빠른 알고리즘은 메모리를 더 많이 써서(동적 계획법),
시간을 절약하거나, 메모리를 절약하여 시간이 더 걸리게 할 수 있다.
재귀함수와 반복문을 비교해보자, 시간 복잡도는 같을 수 있지만, 공간 복잡도는 재귀 함수(스택 메모리 사용)가 더 높을 수 있다.
가령 메모리를 더 사용해서 시간 복잡도를 낮추는 것중에 해시 테이블도 예시로 들 수 있다.
어쨌건 흔히 알려져있는 알고리즘에 대해서
필자가 Big-O를 표로 정리한 것을 보자.
시간 복잡도(최고-평균-최악)
알고리즘 | 최고 (BEST) | 평균 (AVERAGE) | 최악 (WORST) |
|---|---|---|---|
삽입 정렬 (Insertion Sort) | |||
선택 정렬 (Selection Sort) | |||
버블 정렬 (Bubble Sort) | |||
병합 정렬 (Merge Sort) | |||
퀵 정렬 (Quick Sort) | |||
힙 정렬 (Heap Sort) | |||
셸 정렬 (Shell Sort) | |||
계수 정렬 (Counting Sort) | |||
기수 정렬 (Radix Sort) | |||
버킷 정렬 (Bucket Sort) |
- 알고리즘
삽입 정렬 (Insertion Sort)
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
선택 정렬 (Selection Sort)
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
버블 정렬 (Bubble Sort)
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
병합 정렬 (Merge Sort)
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
퀵 정렬 (Quick Sort)
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
힙 정렬 (Heap Sort)
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
셸 정렬 (Shell Sort)
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
계수 정렬 (Counting Sort)
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
기수 정렬 (Radix Sort)
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
버킷 정렬 (Bucket Sort)
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
1. 삽입 정렬 (Insertion Sort)
최고(Best):
이미 정렬된 데이터일 때, 각 요소를 비교하면서도 자리 이동이 필요 없음.
예: [1, 2, 3, 4]는 비교만 하고 삽입하지 않아도 됨.
평균(Average):
데이터가 랜덤하게 분포되어 있을 경우, 매 요소마다 평균 절반 정도의 자리 이동이 필요.
최악(Worst):
역순으로 정렬된 데이터일 때, 매 요소마다 모든 앞 요소와 비교 및 자리 이동이 필요.
예: [4, 3, 2, 1]에서는 각 요소마다 최대 n번의 비교와 이동이 필요.
2. 선택 정렬 (Selection Sort)
최고/평균/최악:
모두
입력 데이터 상태에 관계없이 항상 같은 성능을 보임.
이유: 매 단계마다 남은 배열에서 최솟값(또는 최댓값)을 찾아야 하므로, 비교 횟수는 항상 고정됨.
최고의 경우에도 교환 횟수가 줄어들 뿐, 비교 횟수는 변하지 않음.
3. 버블 정렬 (Bubble Sort)
최고(Best):
이미 정렬된 데이터일 때, 한 번의 스캔으로 교환이 일어나지 않으면 조기 종료 가능.
예: [1, 2, 3, 4]에서는 교환이 없으므로 한 번의 전체 스캔 후 종료.
평균(Average):
랜덤한 데이터에서는 각 요소가 평균 절반 정도의 위치까지 이동해야 함.
최악(Worst):
역순으로 정렬된 데이터일 때, 매 요소마다 최대 n번의 비교와 교환이 필요.
예: [4, 3, 2, 1]에서는 모든 요소가 한 칸씩 이동해야 함.
4. 병합 정렬 (Merge Sort)
최고/평균/최악:
모두
이유: 병합 정렬은 분할(Divide)과 결합(Conquer) 과정에서 항상 동일한 작업을 수행함.
입력 데이터 상태에 관계없이 분할과 병합 과정은 동일하게 진행됨.
5. 퀵 정렬 (Quick Sort)
최고(Best):
피벗(Pivot)이 항상 중간값으로 선택될 때, 균형 잡힌 분할이 이루어짐.
예: [4, 1, 3, 2, 6, 5]에서 피벗이 중간값일 경우.
평균(Average):
대부분의 경우 피벗이 균형 잡힌 위치로 선택되면 O(n log n) 성능을 유지.
최악(Worst):
피벗이 항상 가장 작은 값 또는 가장 큰 값으로 선택될 때, 불균형한 분할이 이루어짐.
예: [1, 2, 3, 4, 5]처럼 이미 정렬된 데이터에서 첫 번째 요소를 피벗으로 선택하면 매번 한쪽만 분할됨.
6. 힙 정렬 (Heap Sort)
최고/평균/최악:
모두
이유: 힙 자료구조를 사용하여 데이터를 추출하는 과정에서 항상 동일한 작업을 수행함.
입력 데이터 상태에 관계없이 힙 구성과 추출 과정은 동일하게 진행됨.
7. 셸 정렬 (Shell Sort)
최고(Best):
간격(Gap)이 잘 설정되고 데이터가 거의 정렬된 상태일 때 효율적.
평균(Average):
일반적인 경우 간격이 점진적으로 줄어들며 데이터가 점차 정렬됨.
물론 셸 정렬의 시간 복잡도는 gap sequence에 따라 달라지고
사이로 알려져 있지만, 일반적으로 의 경험적 측정값이 쓰이기도 한다.
최악(Worst):
간격이 잘못 설정되거나 데이터가 역순으로 정렬된 경우 비효율적.
8. 계수 정렬 (Counting Sort)
최고/평균/최악:
모두
이유: 데이터 범위(k)가 작을 때, 데이터를 직접 계산하여 정렬하므로 입력 데이터 상태에 관계없이 동일한 성능.
단, k가 클 경우 메모리 사용량이 증가함.
9. 기수 정렬 (Radix Sort)
최고/평균/최악:
모두
이유: 각 자리수별로 정렬하므로, 데이터 크기(n)와 자릿수(k)에 비례하여 동일한 작업을 수행함.
입력 데이터 상태에 관계없이 동일한 성능을 보임.
10. 버킷 정렬 (Bucket Sort)
최고(Best):
데이터가 균등하게 분포되어 있고, 버킷 내부 정렬이 필요 없을 때.
평균(Average):
데이터가 대체로 균등하게 분포되어 있지만, 버킷 내부에서 일부 정렬이 필요할 때.
최악(Worst):
데이터가 한 버킷에 집중되어 있을 때, 해당 버킷 내부에서 정렬 비용이 증가함.
공간 복잡도(최고-평균-최악)
알고리즘 | 최고 (BEST) | 평균 (AVERAGE) | 최악 (WORST) |
|---|---|---|---|
삽입 정렬 | |||
선택 정렬 | |||
버블 정렬 | |||
병합 정렬 | |||
퀵 정렬 | |||
힙 정렬 | |||
셸 정렬 | |||
계수 정렬 | |||
기수 정렬 | |||
버킷 정렬 |
- 알고리즘
삽입 정렬
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
선택 정렬
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
버블 정렬
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
병합 정렬
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
퀵 정렬
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
힙 정렬
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
셸 정렬
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
계수 정렬
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
기수 정렬
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
- 알고리즘
버킷 정렬
- 최고 (BEST)
- 평균 (AVERAGE)
- 최악 (WORST)
1. 삽입 정렬, 선택 정렬, 버블 정렬, 힙 정렬, 쉘 정렬
모두 제자리(In-place) 정렬 알고리즘으로, 추가적인 메모리가 거의 필요하지 않는다.
공간 복잡도는 항상
2. 병합 정렬
분할된 배열을 임시 저장하기 위해 추가적인 배열이 필요합
공간 복잡도는 항상 이며, 이는 병합 정렬의 주요 단점 중 하나
3. 퀵 정렬
재귀 호출 스택으로 인해 추가 메모리가 필요합니다.
평균적으로 의 공간이 필요하며, 최악의 경우 까지 증가할 수 있다.
4. 계수 정렬
데이터의 범위(k)에 따라 추가 메모리가 필요
공간 복잡도는
5. 기수 정렬
각 자리수를 기준으로 정렬하므로 데이터 크기(n)와 범위(k)에 비례하는 추가 메모리가 필요
공간 복잡도는
6. 버킷 정렬
데이터를 버킷으로 나누어 저장하므로 데이터 크기와 버킷 개수에 비례하는 추가 메모리가 필요(버킷 자체를 저장하는 데 필요한 공간은 이며,
데이터가 한 버킷에 몰리는 경우는 시간 복잡도에 영향을 준다.)
공간 복잡도는
어쨌건 이 글은 정렬 알고리즘들의 Big O를 수학적으로 정리하는 그런 글이 아니다.
그럼 왜 이 글을 썼을까?
그건 Big-O의 한계를 말하기 위해서다.

모든 모델은 틀리지만, 어떤 것은 유용하다 -George Edward Pelham Box, 수학자
Big-O의 오해와 한계
Big-O는 복잡도를 단순화한 모델일 뿐, 현실의 모든 변수를 담을 수 없다.
단지 알고리즘의 효율성을 빠르게 비교하는 것일뿐이다.
그럼 예시를 들어보자
1. Big-O는 점근적 상한(Upper Bound)이다. Worst Case가 전부가 아니다.
using System;
using System.Diagnostics;
class QuickSortExample
{
// 최악의 경우: 첫 번째 요소를 피벗으로 선택
static void QuickSortWorst(int[] arr, int left, int right)
{
if (left >= right) return;
int pivot = arr[left];
int i = left + 1;
for (int j = left + 1; j <= right; j++)
{
if (arr[j] < pivot)
{
Swap(arr, i, j);
i++;
}
}
Swap(arr, left, i - 1);
QuickSortWorst(arr, left, i - 2);
QuickSortWorst(arr, i, right);
}
// 평균적인 경우: 중간 요소를 피벗으로 선택
static void QuickSortAvg(int[] arr, int left, int right)
{
if (left >= right) return;
int mid = left + (right - left) / 2;
Swap(arr, mid, right);
int pivot = arr[right];
int i = left - 1;
for (int j = left; j < right; j++)
{
if (arr[j] < pivot)
{
i++;
Swap(arr, i, j);
}
}
Swap(arr, i + 1, right);
int pivotIndex = i + 1;
QuickSortAvg(arr, left, pivotIndex - 1);
QuickSortAvg(arr, pivotIndex + 1, right);
}
static void Swap(int[] arr, int i, int j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
static int[] MakeSorted(int n)
{
int[] arr = new int[n];
for (int i = 0; i < n; i++) arr[i] = i;
return arr;
}
static int[] MakeRandom(int n)
{
int[] arr = new int[n];
Random rand = new Random(42);
for (int i = 0; i < n; i++) arr[i] = rand.Next(n);
return arr;
}
static void Main()
{
const int N = 50000;
// JIT 웜업
int[] warmup = MakeRandom(1000);
QuickSortWorst(warmup, 0, warmup.Length - 1);
warmup = MakeRandom(1000);
QuickSortAvg(warmup, 0, warmup.Length - 1);
int[] sorted = MakeSorted(N);
var stopwatch = Stopwatch.StartNew();
QuickSortWorst(sorted, 0, sorted.Length - 1);
stopwatch.Stop();
Console.WriteLine($"최악의 경우 (n={N}, 정렬된 배열 + 첫 요소 피벗): {stopwatch.Elapsed.TotalMilliseconds:F2}ms");
int[] random = MakeRandom(N);
stopwatch.Restart();
QuickSortAvg(random, 0, random.Length - 1);
stopwatch.Stop();
Console.WriteLine($"평균적인 경우 (n={N}, 랜덤 배열 + 중간 요소 피벗): {stopwatch.Elapsed.TotalMilliseconds:F2}ms");
}
}퀵 정렬은 평균적으로 이지만, Worst Case는 다. 관행적으로 Big O를 Worst Case 기준으로 쓸 때가 많다 보니, 퀵 정렬은 알고리즘으로 소개되기도 하지만, 이론적 분류는 이다.
여기서 이론과 현실의 괴리가 생긴다. 위 코드를 실행해보면, 랜덤 배열(평균적 입력)에서 퀵 정렬은 실제로 에 가깝게 동작한다. 라는 표기만 보고 느린 알고리즘이라고 단정할 수 없다는 것이다.
더 나아가, 작은 n에서는 이론상 인 알고리즘이 보다 빠른 경우도 있다. 캐시 지역성(cache locality) 때문이다. Big O는 상수 계수와 캐시 효율을 무시하므로, n이 충분히 크지 않으면 복잡도 등급보다 하드웨어 특성이 성능을 지배한다.
2. 상수 계수(Constant Factor)를 무시한다.
using System;
using System.Diagnostics;
class ConstantCoefficientExample
{
static long F(int n)
{
return 100L * n + 1000; // O(n)
}
static long G(int n)
{
return (long)(0.1 * n * n); // O(n²)
}
static void Main()
{
const int N = 10;
const int ITERATIONS = 10_000_000;
// JIT 웜업
F(N);
G(N);
long dummy = 0;
var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++) dummy += F(N);
stopwatch.Stop();
Console.WriteLine($"O(n) 함수 {ITERATIONS:N0}회 반복: {stopwatch.Elapsed.TotalMilliseconds:F2}ms (dummy={dummy})");
dummy = 0;
stopwatch.Restart();
for (int i = 0; i < ITERATIONS; i++) dummy += G(N);
stopwatch.Stop();
Console.WriteLine($"O(n²) 함수 {ITERATIONS:N0}회 반복: {stopwatch.Elapsed.TotalMilliseconds:F2}ms (dummy={dummy})");
Console.WriteLine($"\nn={N}에서는 상수 계수 100이 붙은 O(n)이 O(n²)보다 느릴 수 있다.");
}
}과 이 있다면, Big O는 각각 과 로 분류한다.
그러나 n이 작을 때는 이 더 빠를 수 있다.
위 코드가 그 사례다. 이면 상수 계수 100이 붙은 이 오히려 보다 느리게 동작한다.
3. 동일한 복잡도라도 실제 성능은 다르다.
using System;
using System.Diagnostics;
class QuickSortExample
{
static void QuickSort(int[] arr, int left, int right)
{
if (left >= right) return;
int mid = left + (right - left) / 2;
Swap(arr, mid, right);
int pivot = arr[right];
int i = left - 1;
for (int j = left; j < right; j++)
{
if (arr[j] < pivot)
{
i++;
Swap(arr, i, j);
}
}
Swap(arr, i + 1, right);
int pivotIndex = i + 1;
QuickSort(arr, left, pivotIndex - 1);
QuickSort(arr, pivotIndex + 1, right);
}
static void Swap(int[] arr, int i, int j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
static void Main()
{
const int N = 1_000_000;
Random rand = new Random(42);
int[] baseArr = new int[N];
for (int i = 0; i < N; i++) baseArr[i] = rand.Next(N);
// JIT 웜업
int[] warmup = new int[1000];
Array.Copy(baseArr, warmup, 1000);
QuickSort(warmup, 0, warmup.Length - 1);
int[] arr = new int[N];
Array.Copy(baseArr, arr, N);
var stopwatch = Stopwatch.StartNew();
QuickSort(arr, 0, arr.Length - 1);
stopwatch.Stop();
Console.WriteLine($"퀵 정렬 (n={N:N0}): {stopwatch.Elapsed.TotalMilliseconds:F2}ms");
}
}using System;
using System.Diagnostics;
class HeapSortExample
{
static void HeapSort(int[] arr)
{
int n = arr.Length;
for (int i = n / 2 - 1; i >= 0; i--)
Heapify(arr, n, i);
for (int i = n - 1; i > 0; i--)
{
Swap(arr, 0, i);
Heapify(arr, i, 0);
}
}
static void Heapify(int[] arr, int n, int i)
{
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) largest = left;
if (right < n && arr[right] > arr[largest]) largest = right;
if (largest != i)
{
Swap(arr, i, largest);
Heapify(arr, n, largest);
}
}
static void Swap(int[] arr, int i, int j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
static void Main()
{
const int N = 1_000_000;
Random rand = new Random(42);
int[] baseArr = new int[N];
for (int i = 0; i < N; i++) baseArr[i] = rand.Next(N);
// JIT 웜업
int[] warmup = new int[1000];
Array.Copy(baseArr, warmup, 1000);
HeapSort(warmup);
int[] arr = new int[N];
Array.Copy(baseArr, arr, N);
var stopwatch = Stopwatch.StartNew();
HeapSort(arr);
stopwatch.Stop();
Console.WriteLine($"힙 정렬 (n={N:N0}): {stopwatch.Elapsed.TotalMilliseconds:F2}ms");
}
}퀵 정렬과 힙 정렬은 평균 시간 복잡도가 모두 O(n log n)으로 동일하다.
그러나 힙 정렬은 상수 계수가 크고 메모리 접근 패턴이 불규칙해 캐시 효율성이 낮다. 위 두 코드를 실행해보면 이론적 복잡도는 같지만 실측 시간은 퀵 정렬이 대체로 빠르다.
이는 Big-O가 무시하는 요소들, 즉 상수 계수, 캐시 지역성(cache locality), 하드웨어 특성이 실제 성능을 결정하기 때문이다. 이전 글에서 다룬 이중 반복문의 row-major vs column-major 접근도 같은 맥락이다.
복잡도는 동일하지만 캐시 미스 증가로 속도 차이가 발생한다.
(눈으로 볼 수 있는 Big-O 시간 복잡도)
마치며...
결국 Big-O는 강력한 도구지만, 항상 정답은 아니다.
우리는 강력한 이론이 모든 것을 해결해줄 것처럼 이론을 숭배하는 경향이 있다.
하지만 이론은 현실에서 추출해낸 일부일 뿐이다.
이론을 문제 해결의 나침반으로 삼되, 그 길이 실제로 맞는지 주변도 살펴볼 필요가 있다.