# 정렬 알고리즘 (Sorting Algorithms)
정렬 알고리즘은 임의의 데이터 집합을 특정 기준에 따라 순서대로 배열하는 알고리즘의 총칭이다. 일반적으로 오름차순 또는 내림차순으로 정렬하며, 데이터의 탐색, 비교, 탐색 시작점의 최적화, 데이터 압축, 중복 제거 등 다양한 응용에 기초가 된다. 정렬 알고리즘은 본질적으로 데이터를 비교하는지 여부, 사용하는 추가 메모리의 양, 입력 데이터의 분포, 그리고 구현의 복잡성에 따라 분류된다. 수학적으로는 주어진 입력 집합의 크기를 n이라고 할 때, 수행하는 연산의 수를 분석하여 시간 복잡도와 공간 복잡도를 평가한다. 대표적인 시간 복잡도는 $O(n \log n)$, $O(n^2)$, $O(n)$ 등의 범주로 구분된다.
정렬의 필요성은 다음과 같은 상황에서 두드러진다.
- 이진 탐색(Bin Search) 등의 탐색 알고리즘의 전제 조건 충족
- 중복 원소의 관리 및 데이터 무결성 보장
- 데이터의 시각적 정렬, 계보학적 정렬, 데이터 압축 전처리 등
정렬 알고리즘의 용어는 영어 표기를 병용해도 무방하며, 아래의 내용을 통해 서로 다른 알고리즘의 특성을 간략히 비교할 수 있다.
- 안정성 (Stability): 동등한 키를 가진 원소들의 초기 순서가 정렬 후에도 유지되는지 여부
- 제자리성 (In-place): 보조 메모리 없이 원소를 제자리에서 정렬하는지 여부
- 비교 기반 여부 (Comparison-based vs Non-Comparison): 원소 간 비교를 통해 정렬하는지 여부
- 병렬성 (Parallelizability): 다중 코어/멀티스레드, GPU 등에서의 병렬 처리 용이성
- 외부 정렬 (External Sorting): 데이터가 메모리 한계로인해 디스크 등 외부 저장소를 활용하는 정렬
---
## 정렬의 분류
- 비교 기반 정렬 (Comparison-Based Sorting)
- 원소 간의 비교를 통해 순서를 결정한다.
- 대표 알고리즘: QuickSort, MergeSort, HeapSort, InsertionSort, SelectionSort, TimSort, IntroSort 등
- 평균적으로 $O(n \log n)$의 시간 복잡도를 목표로 하지만 최악의 경우 $O(n^2)$로 악화될 수 있다.
- 비교 기반 정렬 (Non-Comparison Sorting)
- 비교 없이 원소의 값에 직접 작용하여 정렬한다.
- 대표 알고리즘: CountingSort, RadixSort, BucketSort
- 데이터의 특정 특성(값의 범위, 자리수 등)에 따라 선형 시간 복잡도 $O(n)$ 또는 근접한 수준의 성능을 낼 수 있다.
- 추가 특성
- 안정성에 따른 구분: Stable vs Unstable
- 제자리성에 따른 구분: In-place vs Non-in-place
- 공간 복잡도: 보조 배열의 필요 여부 및 크기
---
## 주요한 시간 복잡도 및 공간 복잡도
- QuickSort
- 평균: $O(n \log n)$
- 최악: $O(n^2)$ (피벗 선택이 항상 최솟값/최댓값으로 떨어지는 경우)
- 공간: 재귀 깊이에 따른 스택 공간, 일반적으로 $O(\log n)$
- 안정성: Unstable
- MergeSort
- 항상: $O(n \log n)$ (일반적인 구현)
- 공간: $O(n)$ (보조 배열 필요)
- 안정성: Stable
- 특징: 분할과 합병의 구조로 인해 안정성과 예측 가능한 시간 복잡도가 장점
- HeapSort
- 항상: $O(n \log n)$
- 공간: $O(1)$ 추가 공간 (제자리 정렬)
- 안정성: Unstable
- InsertionSort
- 최선: $O(n)$ (부분적으로 이미 정렬된 경우)
- 평균/최악: $O(n^2)$
- 공간: $O(1)$
- 안정성: Stable
- SelectionSort
- 항상: $O(n^2)$
- 공간: $O(1)$
- 안정성: Unstable (일부 구현에서 안정적으로 보일 수 있으나 일반적으로 Unstable로 간주)
- CountingSort
- 시간: $O(n + k)$ (k는 값의 범위)
- 공간: $O(k)$
- 안정성: Stable
- 조건: 입력 원소의 값이 제한된 범위에 있을 때 효과적
- RadixSort
- 시간: $O(n \cdot k)$ (k는 숫자의 자릿수 수, LSD 또는 MSD 방식에 따라 다름)
- 공간: 보조 배열 필요
- 안정성: Stable
- 조건: 정수 키나 고정된 길이의 문자열에 효과적
- BucketSort
- 시간: 기대적으로 $O(n)$ (균등 분포 가정 하)
- 공간: $O(n)$
- 안정성: 보조 정렬의 선택에 따라 달라짐
- 조건: 입력이 균등하게 여러 버킷에 분포할 때 효과적
- TimSort
- 시간: $O(n \log n)$ 보장(일반적인 경우)
- 안정성: Stable
- 특징: 삽입 정렬의 귀납과 합병 정렬의 하이브리드로 구현된 실무 최적화 알고리즘
- IntroSort
- 시간: 평균적으로 $O(n \log n)$, 최악의 경우도 보정 가능
- 특징: QuickSort의 빠른 성능과 HeapSort의 악화 방지를 결합한 계통
- 안정성: Unstable
---
## 주요 알고리즘의 특징 및 구현 개요
1) QuickSort
- 목적: 평균적으로 빠른 성능을 보이는 비슷한 크기의 분할 정복 알고리즘
- 아이디어: 배열을 피벗(pivot) 기준으로 좌측과 우측으로 분할한 뒤 재귀적으로 정렬
- 핵심 포인트
- 파티션(Partition) 전략의 선택: Lomuto, Hoare 등
- 피벗 선택 방식에 따라 최악/평균 성능 차이가 커진다
- 안정성과 제자리성의 트레이드오프: 일반적으로 Unstable, In-place 가능
- 간단한 구현 예 (Python)
- In-place를 사용하는 전형적 구현이나, 학습용으로는 재귀적 간단 구현도 흔히 사용
- 예시 코드: 전체 파티션과 재귀 호출 예시
- 수식 예: 평균 경우 시간 복잡도는 $O(n \log n)$이고 최악의 경우 $O(n^2)$
- 주의점: 피벗 선택이 성능에 큰 영향을 미치므로 IntroSort 같은 보강 기법이 실무에서 널리 사용된다
2) MergeSort
- 목적: 안정적이고 예측 가능한 시간 복잡도를 제공하는 분할 정복 알고리즘
- 아이디어: 배열을 절반으로 분할하고, 각 절반을 재귀적으로 정렬한 뒤 합병
- 핵심 포인트
- 안정성 보장: 동일 키의 원소 순서를 유지
- 기본적으로 추가 배열이 필요하여 제자리성이 떨어짐
- 병렬 처리에 적합한 특성
- 간단한 구현 예 (Python)
- 재귀적 구현과 합병 함수의 구성 예시
- 수식 예: 재귀식 $T(n) = 2 T(n/2) + \Theta(n)$, 따라서 $T(n) = \Theta(n \log n)$
- 주의점: 큰 입력에 대해 안정성과 예측 가능한 시간 복잡도를 제공하지만 추가 공간이 필요
3) HeapSort
- 목적: 제자리에서 수행 가능한 정렬로, 최악의 경우에도 $O(n \log n)$를 보장
- 아이디어: 배열을 힙으로 구성하고, 힙에서 최대(또는 최소)를 하나씩 추출하여 정렬
- 핵심 포인트
- 안정성: Unstable
- 제자리성: Yes
- 하이브리드 상황에서의 사용은 피벗 의존도가 낮아 일관된 성능 제공
- 간단한 구현 예 (Python)
- 힙 구성과 정렬 루프를 포함한 예시
- 수식 예: 시간 복잡도 $O(n \log n)$, 공간 $O(1)$
- 주의점: 힙 구조를 이해하고, 인덱스 조작에 주의해야 한다
4) InsertionSort
- 목적: 거의 정렬된 데이터나 작은 데이터셋에서 매우 효율적
- 아이디어: 정렬된 부분 배열에 새로운 원소를 적절한 위치에 삽입
- 핵심 포인트
- 안정성: Stable
- 제자리성: Yes
- 시간 복잡도: 최선 $O(n)$, 최악/평균 $O(n^2)$
- 수식 예: 최선의 경우 $O(n)$, 최악의 경우 $O(n^2)$
5) CountingSort
- 목적: 값의 범위가 한정된 경우 매우 빠르게 정렬
- 아이디어: 각 값의 출현 횟수를 세고 누적합을 이용해 자리 위치를 확정
- 핵심 포인트
- 안정성: Stable
- 제자리성: No (보조 배열 필요)
- 시간 복잡도: $O(n + k)$, 공간: $O(k)$
- 조건: 입력 값의 범위가 작은 경우에만 활용
- 수식 예: 시간 복잡도 $O(n + k)$, 공간 복잡도 $O(k)$
6) RadixSort
- 목적: 비정수 키나 문자열과 같이 다자리 숫자 데이터를 정렬
- 아이디어: LSD(왼쪽에서 오른쪽으로) 또는 MSD 방식으로 각 자리수별로 비교 없이 정렬
- 핵심 포인트
- 안정성: Stable
- 제자리성: 일반적으로 No (보조 버킷/배열 필요)
- 시간 복잡도: $O(n \cdot k)$ (k는 자리수 수)
- 수식 예: 시간 복잡도 $O(n \cdot k)$
7) BucketSort
- 목적: 입력 분포가 균등하면 선형 시간에 근접한 정렬 가능
- 아이디어: 입력을 여러 버킷에 나누고 각 버킷을 정렬한 뒤 합친다
- 핵심 포인트
- 안정성: 보조 정렬의 선택에 따라 달라짐
- 제자리성: No (버킷 관리 필요)
- 조건: 균등 분포 가정이 필요
- 수식 예: 기대 시간 복잡도는 $O(n)$, 불균형 시 악화될 수 있음
8) TimSort
- 목적: 실무에서의 범용 정렬로 널리 채택된 하이브리드 알고리즘
- 아이디어: 작은 이미 정렬된 조각들(런, Run)을 사용한 합병 정렬과 삽입 정렬의 하이브리드
- 핵심 포인트
- 안정성: Stable
- 제자리성: 보조 메모리 필요 여부는 구현에 따라 다름
- 시간 복잡도: 평균/최선/최악 모두 $O(n \log n)$에 근접
- 수식 예: 일반적으로 안전한 시간 복잡도
---
## 구현에 대한 실무 팁
- 피벗 선택의 중요성
- QuickSort의 성능은 피벗 선택에 크게 의존한다. 랜덤 피벗, 중간값 삼분법 등 다양한 전략이 있다.
- 하이브리드 접근
- IntroSort, TimSort 등의 하이브리드 알고리즘은 최악의 경우를 방지하고 실무에서 우수한 성능을 보인다.
- 공간 관리
- 제자리 정렬과 보조 배열 필요 여부를 데이터의 특성과 메모리 제약 조건에 따라 판단한다.
- 안정성의 필요성
- 데이터의 키 외에 추가적인 정렬 키가 존재하는 경우 안정성이 중요할 수 있다.
- 외부 정렬의 필요성
- 대용량 데이터의 경우 외부 정렬(External Sorting)이 필요할 수 있으며, 다단계 버킷/분할 및 외부 합병이 중요한 역할을 한다.
---
## 간단한 구현 예시 (선택 언어: Python)
- QuickSort (간단한 재귀 구현)
```python
def quicksort(a):
if len(a) <= 1:
return a
pivot = a[len(a) // 2]
left = [x for x in a if x < pivot]
middle = [x for x in a if x == pivot]
right = [x for x in a if x > pivot]
return quicksort(left) + middle + quicksort(right)
```
- MergeSort (재귀적 구현)
```python
def mergesort(a):
if len(a) <= 1:
return a
mid = len(a) // 2
left = mergesort(a[:mid])
right = mergesort(a[mid:])
# 합병
i = j = 0
out = []
while i < len(left) and j < len(right):
if left[i] <= right[j]:
out.append(left[i]); i += 1
else:
out.append(right[j]); j += 1
out.extend(left[i:])
out.extend(right[j:])
return out
```
- HeapSort (제자리 구현 예시)
```python
def heapsort(a):
def heapify(a, n, i):
largest = i
l = 2 * i + 1
r = 2 * i + 2
if l < n and a[l] > a[largest]:
largest = l
if r < n and a[r] > a[largest]:
largest = r
if largest != i:
a[i], a[largest] = a[largest], a[i]
heapify(a, n, largest)
n = len(a)
for i in range(n // 2 - 1, -1, -1):
heapify(a, n, i)
for end in range(n - 1, 0, -1):
a[end], a[0] = a[0], a[end]
heapify(a, end, 0)
return a
```
- InsertionSort (간단 예)
```python
def insertionsort(a):
for i in range(1, len(a)):
key = a[i]
j = i - 1
while j >= 0 and a[j] > key:
a[j + 1] = a[j]
j -= 1
a[j + 1] = key
return a
```
- CountingSort (전형 예)
```python
def countingsort(a, max_value):
count = [0] * (max_value + 1)
for x in a:
count[x] += 1
idx = 0
for value, c in enumerate(count):
for _ in range(c):
a[idx] = value
idx += 1
return a
```
---
## 용어 정리
- Stability: 정렬된 결과에서 동등한 키의 원소들이 입력 순서를 보존하는 성질.
- In-place: 추가 메모리 할당 없이 원소를 정렬하는 능력.
- Time Complexity: 알고리즘이 문제의 크기 n에 대해 소요하는 평균/최선/최악의 연산 수를 나타내는 척도.
- Space Complexity: 알고리즘이 추가로 필요한 메모리 공간의 양.
- Non-Comparison Sorting: 값의 범위나 자리수 등 입력 데이터의 특성을 활용해 비교 없이 정렬하는 방법.
---
## 실무에서의 선택 가이드
- 데이터의 크기가 작고 이미 거의 정렬된 경우: InsertionSort나 TimSort 계열이 유리
- 큰 데이터 집합이 있고 평균적으로 빠른 성능이 필요하면: QuickSort(피벗 전략에 의존) 또는 IntroSort
- 키 값의 범위가 작고 안정성이 필요하면: CountingSort
- 큰 데이터 집합이고 안정성과 병렬 처리, 외부 저장이 필요한 경우: MergeSort 계열 또는 TimSort, 외부 정렬 기법
---
## 외부 정렬과 병렬 정렬에 대한 간단한 안내
- 외부 정렬: 디스크 I/O가 주요 비용이 된다. 데이터를 여러 청크로 나눠 각각 정렬한 뒤 병합하는 방식의 다단계 접근이 일반적이다.
- 병렬 정렬: 다중 코어나 GPU를 활용해 파티션별 정렬과 합병 과정을 병렬로 수행한다. 성능 향상은 데이터 분할 방식과 합병 전략에 크게 좌우된다.
---
관련 문서: [[퀵정렬(QuickSort)]], [[병합정렬(MergeSort)]], [[힙정렬(HeapSort)]], [[비교 기반 정렬(Comparison-Based Sorting)]], [[비교 비 기반 정렬(Non-Comparison Sorting)]], [[정렬 알고리즘의 복잡도]]