코딩 테스트에 필수적인 지식인 정렬 알고리즘의 코드와 시간 복잡도를 정리해보려고 한다. 파이썬으로 작성되어 있으며 버블, 카운팅, 선택, 퀵, 삽입, 병합 정렬을 순서대로 설명한다.
개인적인 공부를 위해 정리한 것이므로 틀린 부분이 있을 수 있습니다.
시간 복잡도와 알고리즘 기법
알고리즘 | 평균 시간 복잡도 | 최악 시간 복잡도 | 기법 | 비고 |
---|---|---|---|---|
버블 정렬 | $O(n^2)$ | $O(n^2)$ | 비교와 교환 | 구현이 가장 쉬움 |
카운팅 정렬 | $O(n+k)$ | $O(n+k)$ | 비교환 방식 | n이 작을 때 유리함 |
선택 정렬 | $O(n^2)$ | $O(n^2)$ | 비교와 교환 | 교환 회수가 버블, 삽입정렬보다 작음 |
퀵 정렬 | $O(n \log n)$ | $O(n^2)$ | 분할 정복 | 평균적으로 가장 빠름 |
삽입 정렬 | $O(n^2)$ | $O(n^2)$ | 비교와 교환 | n이 작을 때 유리함 |
병합 정렬 | $O(n \log n)$ | $O(n \log n)$ | 분할 정복 | 연결 List의 경우 가장 효율적인 방법 |
버블 정렬(Bubble Sort)
작은 값과 큰 값이 순서를 바꾸며 큰 값이 제일 뒤로 이동하는 모습이 거품이 떠오르는 모양과 같다 해서 붙여진 이름이다.
def bubblesort(arr):
temp = 0
for i in range(len(arr)):
for j in range(len(arr)-i-1):
if arr[j] > arr[j+1]:
temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
return arr
arr = [5, 6, 2, 1, 4, 3]
print(bubblesort(arr))
카운팅 정렬(Counting Sort)
카운팅 정렬은 배열 원소의 등장 횟수를 세어서(Counting) 정렬하는 방식이다. 주어진 배열 원소의 값이 작을 때는 매우 빠르지만, 원소의 값이 클 경우에는 매우 비효율적이다.
def countingsort(arr):
# 만일 배열의 최대값을 쉽게 구할 수 없다면 소요 시간은 더 늘어남
repo = [0] * (max(arr)+1)
# 원소 등장 횟수 카운팅
for i in range(len(arr)):
repo[arr[i]] += 1
arrtemp = []
for i in range(len(repo)):
for j in range(repo[i]):
arrtemp.append(i)
arr = arrtemp
return arr
arr = [2, 1, 1, 4, 4, 4, 4, 3, 2, 2]
print(countingsort(arr))
선택 정렬(Selection Sort)
주어진 배열에서 최소값을 먼저 찾은 후, 그 최소값을 배열의 맨 앞으로 보낸다. 그 다음 정렬되지 않은 나머지 부분에 대해서 이를 반복하는 방법이다.
def selectionsort(arr):
for i in range(len(arr)):
minInd = i # 최소값을 저장하는 인덱스
temp = 0
for j in range(i, len(arr)):
if arr[minInd] > arr[j]:
minInd = j
temp = arr[i]
arr[i] = arr[minInd]
arr[minInd] = temp
return arr
arr = [5, 6, 2, 1, 4, 3]
print(selectionsort(arr))
퀵 정렬(Quick Sort)
퀵 정렬은 분할 정복(Divide and conquer)방식으로 정렬을 수행하는데, 평균 수행 속도가 빠르기 때문에 가장 많이 쓰이는 정렬 알고리즘 중 하나이다. 기준점은 배열의 시작이나 끝 또는 중간값을 선택하기도 하지만 랜덤으로 정하기도 한다. 수행 순서는 다음과 같다.
- 기준점(Pivot point)을 설정한다.
- 주어진 배열의 시작과 끝을 변수
start
와end
에 저장한다. while
문에서 Pivot point보다 크면 오른쪽으로, 작으면 왼쪽으로 이동시킨다.start
와end
변수의 값을 바꾸어준다. 1~3을start<end
를 만족할 때까지 반복한다.start
와pivot
의 값을 바꾸어준다.pivotsort()
함수를 재귀함수로 호출하여 반복한다.
예제 코드에서는 배열 시작을 기준점으로 잡아 정렬을 수행한다.
def quicksort(arr, start, end):
pivot = arr[start]
left, right = start, end
while(start < end):
# pivot보다 큰 값을 찾는다
while(pivot <= arr[end] and start < end):
end -= 1
# pivot보다 작은 값을 찾는다
while(pivot >= arr[start] and start < end):
start += 1
# 두 값을 바꿔 줌
arr[start], arr[end] = arr[end], arr[start]
# 0~start는 pivot보다 작고, start+1~end까지는 pivot보다 크다
# 따라서 pivot 위치를 바꾸어 줌
arr[left], arr[start] = arr[start], arr[left]
# pivot의 좌측과 우측에 대해서 다시 정렬
if(left < start):
quicksort(arr, left, start-1)
if(right > end):
quicksort(arr, end+1, right)
return arr
arr = [3, 4, 2, 1, 5, 6]
print(quicksort(arr, 0, len(arr)-1))
예제 arr = [3, 4, 2, 1, 5, 6]
로 설명해보자.
pivot==3
이다.while
문에 진입한 다음 첫 번째while
문에서는 오른쪽에서pivot
보다 큰 값은 순서대로6, 5
이므로end==3
이다.- 두 번째
while
에서는 바로 4가 나타나기 때문에pivot
보다 작은 값은 없다. 따라서start==1
이다. - 두 인덱스에 맞추어 값을 바꾼다. 따라서 4와 1이 바뀌어
arr = [3, 1, 2, 4, 5, 6]
이다. - 2~4를 반복한다.
pivot
을arr[start]
와 바꾸어 준다.pivot
기준으로 좌측과 우측에 대해서 재정렬한다.
삽입 정렬(Insertion Sort)
삽입 정렬은 현재 인덱스를 기준으로 앞의 원소들과 비교해 적절한 위치에 현재 인덱스의 값을 삽입하는 정렬 방법이다.
def insertionsort(arr):
for i in range(1, len(arr)):
for j in range(len(arr[0:i])):
if arr[j] > arr[i]:
arr[i], arr[j] = arr[j], arr[i]
return arr
arr = [5, 6, 2, 1, 4, 3]
print(insertionsort(arr))
병합 정렬(Merge Sort)
병합 정렬은 주어진 데이터를 반으로 나누어(Divide) 나누어진 리스트를 정렬(Conquer)한 다음 병합(Merge)하는 방식으로 이루어진다.
예제 코드는 리스트를 분할하는 부분과 합치는 부분으로 나누어져 있다. 먼저 mergesort
에서 각 리스트를 1개 단위로 나누어질 때까지 분할하고, merge
를 통해 합치면서 정렬한다. 이 과정을 반복해서 최종적으로 정렬된 리스트를 얻게 된다.
def merge(left, right):
lenl, lenr = len(left), len(right)
result = [0]*(lenl + lenr)
i=j=k=0
while lenl > i and lenr > j:
if left[i] < right[j]:
result[k] = left[i]
i+=1
else:
result[k] = right[j]
j+=1
k+=1
result[k:] = right[j:] if lenl-i<lenr-j else left[i:]
return result
def mergesort(arr):
if len(arr) <= 1:
return arr
middle = len(arr)//2
leftlist = arr[:middle]
rightlist = arr[middle:]
leftlist = mergesort(leftlist)
rightlist = mergesort(rightlist)
return merge(leftlist, rightlist)
arr = [3, 4, 2, 1, 5, 6]
print(mergesort(arr))
병합 정렬은 시간 복잡도가 #O(n log n)#로 보장되기 때문에 강력한 알고리즘이다. 하지만 재귀함수 호출 깊이가 배열의 크기에 따라서 늘어나기 때문에 메모리를 너무 많이 소모하는 단점이 있다. 이 다음에 소개할 힙 정렬은 이런 메모리 관리 문제를 해결했다.
힙 정렬(Heap Sort)
힙 정렬은 이진 트리를 이용한 정렬 방법이다. 이를 이해하려면 먼저 힙 구조, 정확히는 최대 힙 구조를 이해해야 한다. 최대 힙 구조란 부모가 자식보다 값이 큰 이진 트리이다.
먼저 임의의 배열 arr = [3, 4, 2, 1, 5, 6]
을 힙 구조로 배열한다. 그러면 가장 위의 root
에는 배열의 최대값이 위치한다. 이를 힙 구조의 맨 마지막 값과 바꾸어주고, 맨 마지막 값을 제외한 나머지 부분에 대해서 다시 힙 구조화(heapify
)를 수행한다. 이를 모든 원소에 대해서 반복해주면 전체 원소가 정렬된다.
첫 번째 방법
첫 번째 방법은 힙 모듈을 사용하지 않고 최대 힙을 구현하는 방식이다.
def heapify(arr):
for i in range(len(arr)):
child = i
while(child != 0):
root = (child-1)//2
if arr[root] < arr[child]:
arr[child], arr[root] = arr[root], arr[child]
child = root
return arr
def heapsort(arr):
arr = heapify(arr)
for i in range(len(arr)):
arr[len(arr)-(i+1)], arr[0] = arr[0], arr[len(arr)-(i+1)]
arr[:len(arr)-(i+1)] = heapify(arr[:len(arr)-(i+1)])
return arr
arr = [3, 4, 2, 1, 5, 6]
print(heapsort(arr))
두 번째 방법
두 번째 방법은 heapq
모듈을 사용하는 방식이다. heapq
모듈은 최소 힙을 만들어주는데, heappop()
을 사용해 가장 작은 원소부터 꺼내주면 정렬이 완성된다.
import heapq
def heapsort(arr):
heap = []
for ele in arr:
heapq.heappush(heap, ele)
sortedarr = []
while heap:
sortedarr.append(heapq.heappop(heap))
return sortedarr
arr = [3, 4, 2, 1, 5, 6]
print(heapsort(arr))