QuickSort는 분할 정복(divide and conquer) 전략을 사용하는 효율적인 정렬 알고리즘 중 하나다. 평균적인 시간 복잡도는 O(n log n)이며, 최악의 경우 시간 복잡도는 O(n^2)이다.
작동 원리
- 분할(Divide): 배열에서 '피벗'이라고 하는 하나의 원소를 선택한다. 피벗의 위치는 다양한 방법으로 선택할 수 있으며, 피벗 선택 방식에 따라 알고리즘의 성능이 달라질 수 있다. 이 피벗을 기준으로 배열을 두 부분으로 나눈다. 피벗보다 작은 모든 요소는 피벗의 왼쪽에, 피벗보다 큰 모든 요소는 피벗의 오른쪽에 오도록 한다.
- 정복(Conquer): 피벗을 제외한 왼쪽 부분 배열과 오른쪽 부분 배열을 재귀적으로 정렬한다.
- 결합(Combine): QuickSort는 정복 단계에서 배열을 실제로 정렬하므로, 별도의 결합 단계가 필요하지 않다.
특징
- In-place 정렬: 추가적인 배열을 필요로 하지 않으며, 주어진 배열 내에서 정렬을 수행한다.
- 불안정 정렬(Unstable sort): 동일한 값의 원소가 있을 경우, 원래의 순서가 유지되지 않을 수 있다.
- 피벗 선택 중요: 피벗 선택 방법에 따라 성능이 크게 달라질 수 있다. 예를 들어, 항상 첫 번째 원소를 피벗으로 선택하면 이미 정렬된 배열에 대해 최악의 성능(O(n^2))을 보일 수 있다.
def quicksort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quicksort(left) + middle + quicksort(right)
정렬 과정
- 초기 배열: [3, 6, 8, 10, 1, 2, 1]
- 피벗 선택: 8 (배열의 중간 요소)
- 분할:
- 왼쪽(left): [3, 6, 1, 2, 1] (피벗보다 작은 요소)
- 중간(middle): [8] (피벗과 같은 요소)
- 오른쪽(right): [10] (피벗보다 큰 요소)
- 왼쪽(left) 부분 배열에 대한 재귀적 정렬:
- 피벗 선택: 1
- 분할:
- 왼쪽: [1] (피벗보다 작거나 같은 요소)
- 중간: [1] (피벗과 같은 요소)
- 오른쪽: [3, 6, 2] (피벗보다 큰 요소)
- 오른쪽 부분 배열에 대한 재귀적 정렬:
- 피벗 선택: 6
- 분할:
- 왼쪽: [3, 2] (피벗보다 작은 요소)
- 중간: [6] (피벗과 같은 요소)
- 오른쪽: [] (피벗보다 큰 요소가 없음)
- 왼쪽 부분 배열에 대한 재귀적 정렬:
- 피벗 선택: 2
- 분할:
- 왼쪽: [2] (피벗보다 작은 요소)
- 중간: [] (피벗과 같은 요소가 없음)
- 오른쪽: [3] (피벗보다 큰 요소)
- 재조합: [2, 3, 6]
- 재조합: [1, 1, 2, 3, 6]
- 전체 배열에 대한 재조합: [1, 1, 2, 3, 6, 8, 10]
정렬된 배열
- 최종 결과: [1, 1, 2, 3, 6, 8, 10]
MergeSort는 분할 정복(divide and conquer) 방법론을 기반으로 하는 효율적인 정렬 알고리즘이다. 평균적으로 O(n log n)의 시간 복잡도를 가지며, 최악의 경우에도 O(n log n)을 유지한다. 이 알고리즘은 배열을 더 이상 나눌 수 없을 때까지 반으로 나눈 다음, 나누어진 배열을 다시 정렬하면서 병합하는 방식으로 작동한다.
작동 원리
- 분할(Divide): 입력 배열을 가장 작은 단위가 될 때까지 계속 반으로 나눈다. 이 과정은 재귀적으로 수행되며, 각 부분 배열이 하나의 요소만을 갖게 될 때까지 계속된다.
- 정복(Conquer): 나누어진 부분 배열을 다시 병합하면서 정렬한다. 이때, 인접한 두 부분 배열을 정렬하면서 병합하는 방식으로 진행한다.
- 결합(Combine): 모든 부분 배열이 병합되어 하나의 정렬된 배열이 될 때까지 2단계를 반복한다.
특징
- 안정 정렬(Stable sort): 동일한 값의 요소 사이의 상대적인 순서가 변경되지 않는다.
- 비교적 메모리 사용량이 많음: 병합 과정에서 추가적인 배열이 필요하기 때문에, QuickSort와 같은 일부 다른 정렬 알고리즘에 비해 메모리 사용량이 많다.
- 분할 정복 알고리즘: 데이터를 분할하고, 각각을 정복한 다음, 결과를 결합하는 전형적인 분할 정복 접근 방식을 따른다.
def mergeSort(arr):
if len(arr) > 1:
mid = len(arr)//2
L = arr[:mid]
R = arr[mid:]
mergeSort(L)
mergeSort(R)
i = j = k = 0
while i < len(L) and j < len(R):
if L[i] < R[j]:
arr[k] = L[i]
i += 1
else:
arr[k] = R[j]
j += 1
k += 1
while i < len(L):
arr[k] = L[i]
i += 1
k += 1
while j < len(R):
arr[k] = R[j]
j += 1
k += 1
단계 | 과정 | 설명 |
1 | [38, 27, 43, 3, 9, 82, 10] | 초기 배열 |
2 | [38, 27, 43] & [3, 9, 82, 10] | 배열을 반으로 나눈다. |
3 | [38] & [27, 43] | 왼쪽 부분 배열을 다시 반으로 나눈다. |
4 | [27] & [43] | [27, 43]을 다시 반으로 나눈다. |
5 | [27, 43] | [27]과 [43]을 병합하여 정렬한다. |
6 | [27, 38, 43] | [38]과 [27, 43]을 병합하여 정렬한다. |
7 | [3] & [9, 82, 10] | 오른쪽 부분 배열을 반으로 나눈다. |
8 | [9] & [82, 10] | [9, 82, 10]을 다시 반으로 나눈다. |
9 | [82] & [10] | [82, 10]을 다시 반으로 나눈다. |
10 | [10, 82] | [82]와 [10]을 병합하여 정렬한다. |
11 | [9, 10, 82] | [9]와 [10, 82]을 병합하여 정렬한다. |
12 | [3, 9, 10, 82] | [3]와 [9, 10, 82]을 병합하여 정렬한다. |
13 | [3, 9, 10, 27, 38, 43, 82] | 최종적으로 모든 부분 배열을 병합하여 정렬한다. |
HeapSort는 힙(heap) 자료구조를 기반으로 하는 효율적인 정렬 알고리즘 중 하나다. 이 알고리즘은 최악, 평균, 최선의 경우 모두 O(n log n)의 시간 복잡도를 가진다. HeapSort의 기본 아이디어는 주어진 데이터를 이진 힙(binary heap) 구조로 만든 후, 큰 값들(또는 작은 값들)을 배열의 끝으로 이동시키면서 정렬하는 것이다.
힙(Heap) 이란?
힙은 각 노드의 값이 자식 노드의 값보다 크거나 같은(또는 작거나 같은) 완전 이진 트리(complete binary tree)이다. 힙은 크게 두 종류가 있다: 최대 힙(max heap)과 최소 힙(min heap). 최대 힙에서는 부모 노드의 값이 자식 노드의 값보다 항상 크거나 같으며, 최소 힙에서는 부모 노드의 값이 자식 노드의 값보다 항상 작거나 같다.
HeapSort의 작동 방식
- 힙 구성(Heapify): 주어진 배열을 최대 힙 형태로 구성한다. 이 과정은 배열의 중간부터 시작하여 루트 노드까지 거슬러 올라가면서, 각 노드를 올바른 위치로 이동시키는 "heapify" 과정을 반복하여 수행한다.
- 정렬(Sort): 최대 힙의 루트(가장 큰 값)는 항상 힙의 첫 번째 위치에 있으므로, 이를 배열의 마지막 요소와 교환한다. 배열에서 힙으로 고려되는 부분의 크기를 하나 줄이고(실제로 배열에서 요소를 제거하는 것은 아니다), 변경된 힙에 대해 다시 heapify 과정을 수행한다. 이 과정을 배열이 정렬될 때까지 반복한다.
특징
- In-place 정렬: 추가적인 배열을 필요로 하지 않으며, 주어진 배열 내에서 정렬을 수행한다.
- 불안정 정렬(Unstable sort): 동일한 값의 원소가 있을 경우, 원래의 순서가 유지되지 않을 수 있다.
- 효율적인 시간 복잡도: 모든 경우에 O(n log n)의 시간 복잡도를 제공한다.
def heapify(arr, n, i):
largest = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and arr[largest] < arr[left]:
largest = left
if right < n and arr[largest] < arr[right]:
largest = right
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)
def heapSort(arr):
n = len(arr)
for i in range(n//2 - 1, -1, -1):
heapify(arr, n, i)
for i in range(n-1, 0, -1):
arr[i], arr[0] = arr[0], arr[i]
heapify(arr, i, 0)
힙 구성(Heapify) 단계
단계 | 과정 | 설명 |
1 | [3, 9, 82, 10, 38, 27, 43] | 초기 배열 |
2 | [3, 9, 82, 10, 38, 27, 43] | 배열을 최대 힙으로 만들기 시작 (아직 변화 없음) |
3 | [3, 9, 82, 10, 38, 27, 43] | 자식 노드들과 비교하여 최대 힙 속성 만족 (아직 변화 없음) |
4 | [3, 9, 82, 10, 38, 27, 43] | 더 큰 자식 노드와 부모 노드를 계속 교환 |
5 | [82, 9, 43, 10, 38, 27, 3] | 최대 힙 구성 완료 |
정렬(Sort) 단계
단계 | 과정 | 설명 |
6 | [3, 9, 43, 10, 38, 27, 82] | 최대 값(82)을 배열의 끝으로 이동 후 힙 크기 감소 |
7 | [43, 9, 27, 10, 38, 3, 82] | 새로운 최대 힙 구성 |
8 | [9, 10, 27, 3, 38, 43, 82] | 다음 최대 값(43)을 배열의 끝으로 이동 후 힙 크기 감소 |
9 | [38, 10, 27, 3, 9, 43, 82] | 계속해서 최대 힙 구성 및 최대 값을 배열 끝으로 이동 |
10 | [10, 9, 27, 3, 38, 43, 82] | 최대 힙 구성 및 최대 값을 배열 끝으로 이동 |
11 | [27, 9, 10, 3, 38, 43, 82] | 반복 |
12 | [9, 3, 10, 27, 38, 43, 82] | 최종적으로 정렬된 배열 |
'알고리즘' 카테고리의 다른 글
LCS(Longest Common Subsequence), Knapsack (0) | 2024.04.03 |
---|---|
Dijkstra's Algorithm, Floyd-Warshall Algorithm (0) | 2024.04.02 |
Dynamic Programming, Greedy Algorithm (0) | 2024.04.02 |
BFS, DFS, Topological Sort (2) | 2024.04.01 |
Brute-Force Search, Binary Search, Priority Queue (0) | 2024.03.30 |