[알고리즘] 병합 정렬, 퀵 정렬 (with 재귀 함수)
2026. 6. 11. 23:28ㆍKDT/과제
더보기
1. 재귀함수란
2. 분할 정복이란
3. 병합 정렬
- 병합 정렬이란
- 시간복잡도와 공간복잡도
- 구현
3. 퀵 정렬
- 퀵 정렬이란
- 시간복잡도와 공간복잡도
- 구현
1. 재귀 함수 Recursive Function
- 함수 내부에서 자기 자신을 다시 호출하여 작업을 수행하는 프로그래밍 기법 (예: 팩토리얼(Factorial))
- 구성요소
- 기본 사례(Base Case) : 재귀호출을 멈추고 값을 반환하는 종료 조건
-> 종료 조건이 없으면 무한 루프에 빠지거나 스택 오버플로우가 발생할 수 있음 - 재귀 사례(Recursive Case) : 함수가 자기 자신을 호출하는 조건 및 연산
- 기본 사례(Base Case) : 재귀호출을 멈추고 값을 반환하는 종료 조건
- 장점 : 코드 간결. 가독성 굳. 복잡한 알고리즘을 단순한 논리로 표현
- 단점 :
- 함수 호출될 때마다 호출 정보가 메모리 스택에 쌓임
- 종료 조건이 잘못되거나 데이터가 너무 크면 스택 오버플로우 에러가 발생할 수 있음
- 일반함수와 재귀함수 차이점
| 일반 함수 | 재귀 함수 | |
| 메모리 | 변수 재사용. 추가 메모리 할당 없음 | 호출마다 메모리 새로 할당 |
| 속도 | 상대적으로 빠름 | 함수 호출 오버헤드로 상대적으로 느림 |
| 위험성 | 무한 루프 시 CPU 점유율이 올라갈 수 있으나 메모리 에러는 드묾 | 종료 조건이 없으면 스택 오버플로우(메모리 부족) |
2. 분할 정복 Divide and Conquer
- 분할(Divide) : 해결하기 힘든 거대한 문제를 유사한 형태의 여러 하위 문제로 쪼개고
- 정복(Conquer) : 각각을 재귀적으로 해결한 뒤
- 결합(Combine) : 그 결과를 합쳐
- 최종 해답을 얻어내는 알고리즘 설계 기법
- 분할 정복을 활용한 알고리즘 : 병합 정렬(합병 정렬), 퀵 정렬, 이진 탐색, 거듭제곱 연산
- 장점 : 복잡하고 거대한 문제를 효율적으로 해결 가능
- 단점 : 재귀 함수 사용으로 스택 오버플로우가 발생할 수 있음
3. 병합 정렬 Merge Sort

1. 병합 정렬이란
- 하나의 큰 문제를 두 개의 작은 문제로 분할한 뒤 각자 계산하고 나중에 합치는 방법
- 동작 방식
- 배열의 길이는 최소 1보다 커야 한다.
(= 배열 길이가 1 이하이면 이미 정렬된 것으로 본다.) - 분할(Divide) : 정렬되지 않은 배열을 절반으로 잘라 비슷한 크기의 두 파티션으로 나눈다. (원소가 2개가 남을 때까지 분할)
- 정복(Conquer) : 분할이 완료되면 나눈 각 파티션을 재귀적으로 호출해 정렬한다.
- 결합(Combine) : 정렬이 완료되면 나누었던 두 파티션을 하나의 정렬된 배열로 병합한다. (이 때 정렬된 배열이 임시 배열에 저장된다)
- 복사(Copy) : 임시 배열에 저장된 결과를 원래 배열에 복사한다.
- 배열의 길이는 최소 1보다 커야 한다.
- 장점 :
- 연결리스트를 사용할 경우, 링크 인덱스만 변경하면 되므로 이동 연산이 줄어듦 (추가 메모리가 필요하지 않음)
- 퀵 정렬과 비교했을 때 데이터 분포에 영향을 받지 않음
- 단점 :
- 배열을 사용할 경우, 추가적인 메모리(임시 배열)이 필요
- 이동 연산이 많아, 레코드 개수가 클 경우에는 비효율적
2. 시간복잡도와 공간복잡도
- 시간복잡도 : O(n log n)
- 배열의 개수가 1개가 될 때까지 분할 하므로 n번의 합병
- 각 합병 단계마다 원소 개수 만큼의 비교와 이동이 이루어짐
- 배열을 반씩 분할해가며 정렬
- 공간복잡도 : O(n)
- 데이터 개수가 n일 때, 정렬과정에서 추가적 메모리(임시 배열) 필요
3. 구현
- 크게 분할 및 정복을 수행하는 함수(merge_sort)와 결합(merge)을 수행하는 함수로 나눈다
- merge_sort를 먼저 만든다
- 배열의 길이가 1이 될 때까지 재귀함수를 호출한다
- 비슷한 크기로 나누기 위해 mid 변수를 생성한다
- mid를 기준으로 배열을 left, right로 나눠 임시 저장한다
- left와 right를 계속 분할한다
- merge 함수를 사용해 최종 정렬을 해준다
- 배열의 길이가 1이라면 배열 그대로 반환한다
- merge를 만든다
- 마지막으로 최종 정렬된 결과를 담을 리스트 변수(result)를 하나 생성한다
- left, right의 각 배열의 현재 위치를 가리키는 인덱스를 생성한다
- left 배열의 인덱스가 배열 길이보다 작고, right 배열의 인덱스도 배열 길이보다 작은 동안 반복한다
- left, right 배열의 값을 비교하고 더 작은 값을 result에 넣고 인덱스에 1을 더한다 (다음 데이터로)
- result에 나머지 배열을 모두 담고 반환한다
# 합병 정렬
# 1. 분할 및 정복 함수
def merge_sort(arr):
if len(arr) > 1:
mid = len(arr) // 2
left = arr[:mid]
right = arr[mid:]
return merge(merge_sort(left), merge_sort(right))
return arr
# 2. 결합 함수
def merge(left, right):
result = [] # 최종 정렬된 결과를 담을 리스트
i, j = 0, 0 # 각 배열의 현재 위치를 가리키는 인덱스
while i < len(left) and j < len(right):
if left[i] < right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result = result + left[i:] # if문으로 만들어도 좋음
result = result + right[j:]
return result
# 합병 정렬 수행
print(merge_sort([3, 1, 4, 1, 5])) # [1, 1, 3, 4, 5]
print(merge_sort([5, 4, 3, 2, 1])) # [1, 2, 3, 4, 5]
print(merge_sort([1])) # [1]
4. 퀵 정렬 Quick Sort

1. 퀵 정렬이란
- 불안정 정렬에 속하며, 다른 원소와의 비교만으로 매우 빠르게 정렬을 수행하는 알고리즘
- 동작방식
- 피벗을 지정한다.
(첫 번째 원소, 마지막 원소, 중간값, 무작위, 듀얼 피벗) - 분할(Divide) : 배열을 피벗 기준으로 비균등하게 2개의 부분 배열로 분할한다.
-> 피벗을 중심으로 왼쪽: 피벗보다 작은 요소들 / 오른쪽: 피벗보다 큰 요소들 - 정복(Conquer) : 부분 배열을 정렬한다. 부분 배열의 크기가 충분히 작지 않으면 순환 호출을 이용하여 다시 분할 정복 방법을 적용한다.
- 결합(Combine) : 정렬된 부분 배열들을 하나의 배열로 합병한다.
- 배열 크기가 0이나 1이 될 때까지 반복한다.
- 피벗을 지정한다.
- 장점 : 배열 내에서 교환하는 방식(제자리 정렬)으로 추가적인 메모리 공간을 필요로 하지 않음
- 단점 : 정렬된 배열에 대해서는 불균형 분할로 인해 더 오랜 시간이 소요됨
2. 시간복잡도와 공간복잡도
- 시간복잡도 : 평균 O(n log n) / 최악 O(n²)
- 배열의 개수가 1이 될 때까지 분할하므로 log(n)번 분할
또한 분할할 때마다 피벗 기준으로 정렬되기 때문에, n번의 비교 연산이 이루어짐 -> 평균 O(n log n) - 이미 정렬된 배열일 경우 분할이 n번만큼 이루어짐 -> 최악 O(n²)
- 배열의 개수가 1이 될 때까지 분할하므로 log(n)번 분할
- 공간복잡도 : O(n)
- 데이터 개수가 n일 때, 주어진 배열 내에서 교환이 이루어짐
3. 구현
- 병합 정렬과 마찬가지로 배열의 길이가 1이 될 때까지 반복한다
- 배열의 첫 요소를 피벗으로 선택하고 저장할 변수(p)를 생성한다
- 리스트 컴프리헨션을 사용하여 left 배열에는 p보다 작은 수들을, middle에는 p와 같은 수들을, right 배열에는 p보다 큰 수들을 담는다
- 이를 반복한다
- left, middle, right를 병합하여 리턴한다
# 퀵 정렬
def quick_sort(arr):
if len(arr) > 1:
p = arr[0] # 배열의 첫 요소를 피벗으로 선택
left = [x for x in arr if x < p]
middle = [x for x in arr if x == p]
right = [x for x in arr if x > p]
return quick_sort(left) + middle + quick_sort(right)
return arr
print(quick_sort([3, 1, 4, 1, 5])) # [1, 1, 3, 4, 5]
* 정렬 알고리즘들은 코드를 읽어보기만 했지 직접 구현해보는 것은 처음이었다. 이번 과제도 시간이 꽤 소요되었지만 그만큼 분명 얻어가는 것은 있다. 재귀 함수는 당연히 종료 조건이 있어야 한다. 알고리즘 수업에서 종료 조건을 채우는 문제를 틀렸던 기억 덕분에 잘 기억하고 있었다. 분할 정복이라는 개념도 알고 있었지만 한 번 더 정리하여 어떤 알고리즘에 쓰이는지 확실하게 알게 되었다.
'KDT > 과제' 카테고리의 다른 글
| [알고리즘] 버블 정렬, 선택 정렬, 삽입 정렬 (0) | 2026.06.10 |
|---|---|
| [13일차 과제] 자료구조(힙) (0) | 2026.06.08 |
| [12일차 과제] 시간복잡도, 공간복잡도 (0) | 2026.06.07 |
| [11일차 과제] 자료구조(해시 테이블) (0) | 2026.06.04 |
| [10일차 과제] 자료구조(트리) (0) | 2026.06.03 |