병합정렬은 분할 정복 (Divide and Conquer) 방식을 이용해서 하나의 리스트를 두 개의 리스트로 분할한 다음 각각의 분할된 리스트를 정렬한 후에 합해서 정렬된 하나의 리스트로 만드는 정렬 알고리즘이다. 재귀 알고리즘을 이용해서 구현 할 수 있다.
평균 : O(nlog₂n)
최악 : O(nlog₂n)
최선 : O(nlog₂n)
❓ 퀵정렬보다 병합정렬이 유리한 경우가 있을까? (자바)
만약 레코드를 연결 리스트(Linked List)로 구성하면, 링크 인덱스만 변경되므로 데이터의 이동은 무시할 수 있을 정도로 작아진다. 합병 정렬은 순차적인 비교로 정렬을 진행하므로, LinkedList의 정렬이 필요할 때, 사용하면 효율적이다. LinkedList를 퀵 정렬에 사용해 정렬하면 성능이 좋지 않다. 이유는 퀵 정렬은 순차 접근이 아닌 임의 접근이기 때문이다. LinkedList는 삽입과 삭제 연산에서는 유용하지만, 접근 연산에서는 비효율적이다.
👉 따라서 크기가 큰 레코드를 정렬할 경우에 연결 리스트를 사용한다면, 합병 정렬은 퀵 정렬을 포함한 다른 어떤 정렬 방법보다 효율적이다.
def mergeSort(arr):
if len(arr) < 2:
return arr
mid = len(arr)//2
low_arr = mergeSort(arr[:mid])
high_arr = mergeSort(arr[mid:])
merge_arr = []
l = h = 0
while l < len(low_arr) and h < len(high_arr):
if(low_arr[l] < high_arr[h]):
merge_arr.append(low_arr[l])
l += 1
else:
merge_arr.append(high_arr[h])
h += 1
merge_arr += low_arr[l:]
merge_arr += high_arr[h:]
return merge_arr
위 코드의 경우 리스트를 분할 할 때마다 새로운 배열을 파이썬의 slice를 이용해서 생성하므로 메모리의 사용효율이 좋지 않다. 아래 코드에서는 인덱스의 값을 참조
def mergeSort(arr):
def sort(low, high):
if high - low < 2:
return
mid = (low + high) // 2
sort(low, mid)
sort(mid, high)
merge(low, mid, high)
def merge(low, mid, high):
temp = []
l, h = low, mid
while l < mid and h < high:
if arr[l] < arr[h]:
temp.append(arr[l])
l += 1
else:
temp.append(arr[h])
h += 1
while l < mid:
temp.append(arr[l])
l += 1
while h < high:
temp.append(arr[h])
h += 1
for i in range(low, high):
arr[i] = temp[i - low]
return sort(0, len(arr))
def quick_sort(array, start, end):
if start >= end:
return
pivot = start # 피벗 초기값은 첫 번째 요소
left = start + 1
right = end
while left <= right:
while left <= end and array[left] <= array[pivot]:
left += 1
while right > start and array[right] >= array[pivot]:
right -= 1
if left > right: # 엇갈렸다면 작은 데이터를 피벗과 교체
array[right], array[pivot] = array[pivot], array[right]
else: # 엇갈리지 않았다면 작은 데이터와 큰 데이터를 교체
array[left], array[right] = array[right], array[left]
quick_sort(array, start, right - 1)
quick_sort(array, right + 1, end)
def quick_sort(array):
if len(array) <= 1:
return array
pivot = array[0] # 피벗은 첫 번째 원소
tail = array[1:] # 피벗을 제외한 리스트
left_side = [x for x in tail if x <= pivot] # 분할된 왼쪽 부분
right_side = [x for x in tail if x > pivot] # 분할된 오른쪽 부분
return quick_sort(left_side) + [pivot] + quick_sort(right_side)
두 구현 방식 모두 평균 시간 복잡도는 O(n log n)입니다. 그러나 In-Place Quick Sort는 추가적인 리스트를 생성하지 않으므로 실제 실행 시간에서 약간 더 효율적일 수 있습니다.
In-Place Quick Sort: 구현이 약간 더 복잡하고, 특히 피벗을 기준으로 요소를 교환하는 과정에서 실수를 할 가능성이 있습니다. 그러나 메모리를 덜 사용합니다.
Functional Quick Sort: 구현이 더 직관적이고 이해하기 쉽습니다. 파이썬의 list comprehension을 사용하여 간결하게 작성할 수 있습니다. 그러나 메모리 사용량이 더 많습니다.
실제 사용에서는 메모리 사용량이 큰 문제가 되지 않는 한, 코드의 가독성과 유지보수성을 고려하여 함수형 스타일의 구현을 사용하는 것도 좋은 선택일 수 있습니다. 성능이 중요한 상황에서는 In-Place Quick Sort를 사용하는 것이 더 효율적일 수 있습니다
기수 정렬(Radix Sort)은 정수나 문자열과 같은 비교가 아닌 정렬 방식을 사용하는 알고리즘입니다. 이 알고리즘은 데이터의 자릿수나 문자의 위치를 기준으로 그룹을 나누어 정렬하는 방식을 취합니다. 기수 정렬은 특히 정수를 정렬할 때 효율적이며, 내부적으로 안정적인 카운팅 정렬(Counting Sort)이나 버킷 정렬(Bucket Sort)을 사용하여 각 자릿수별로 정렬을 수행합니다. 기수 정렬의 시간 복잡도는 O(nk)로, n은 정렬할 원소의 수, k는 원소의 최대 자릿수를 의미합니다.
기수 정렬의 과정은 다음과 같습니다:
1. 가장 낮은 자릿수부터 시작하여 가장 높은 자릿수까지 순차적으로 정렬을 수행합니다.
2. 각 자릿수에 대해 안정적인 정렬 알고리즘(예: 카운팅 정렬)을 사용하여 정렬합니다.
3. 모든 자릿수에 대한 정렬이 완료되면, 전체 데이터가 정렬됩니다.
정렬하기 전: [170, 45, 75, 90, 802, 24, 2, 66]
def counting_sort(arr, exp1):
n = len(arr)
output = [0] * n
count = [0] * (10)
for i in range(0, n):
index = arr[i] // exp1
count[index % 10] += 1
for i in range(1, 10):
count[i] += count[i - 1]
i = n - 1
while i >= 0:
index = arr[i] // exp1
output[count[index % 10] - 1] = arr[i]
count[index % 10] -= 1
i -= 1
for i in range(0, len(arr)):
arr[i] = output[i]
def radix_sort(arr):
max1 = max(arr)
exp = 1
while max1 // exp > 0:
counting_sort(arr, exp)
exp *= 10
arr = [170, 45, 75, 90, 802, 24, 2, 66]
radix_sort(arr)
print(arr)
https://www.daleseo.com/sort-quick/
https://gmlwjd9405.github.io/2018/05/10/algorithm-quick-sort.html
https://akdl911215.tistory.com/386
https://hyen4110.tistory.com/55
https://wikidocs.net/233714
https://10000cow.tistory.com/entry/%EC%A0%95%EB%A0%AC-7-%EA%B8%B0%EC%88%98-%EC%A0%95%EB%A0%ACradix-sort