Algorithm

Sort (Selection, Insertion, Quick, Count)

graygreat 2021. 1. 28. 00:02
728x90
반응형

정렬이란 데이터를 특정한 기준에 따라서 순서대로 나열 하는 것을 말한다.

선택 정렬 (Selection Sort)

데이터가 무작위로 여러 개 있을 때, 이 중에서 가장 작은 데이터를 선택해 맨 앞에 있는 데이터와 바꾸고, 그 다음 작은 데이터를 선택해 앞에서 두 번째 데이터와 바꾸는 과정이다.

1. [7] 5 9 (0) 3 1 6 2 4 8 #가장 작은 0을 선택해 맨 앞에 있는 7과 바꾼다.
2. 0 [5] 9 7 3 (1) 6 2 4 8 #정렬된 첫 번째는 제외하고 이후 데이터 중에서 가장 작은 데이터인 1을 선택해서 처리되지 않은 데이터 중 가장 앞에 있는 데이터 5와 바꾼다.
3. 0 1 [9] 7 3 5 6 (2) 4 8 #정렬된 첫 번째는 제외하고 이후 데이터 중에서 가장 작은 데이터인 2을 선택해서 처리되지 않은 데이터 중 가장 앞에 있는 데이터 9와 바꾼다.
...
10. 0 1 2 3 4 5 6 7 8 (9) #가장 작은 데이터를 앞으로 보내는 과정을 9번 반복한 상태는 다음과 같으며 마지막 데이터는 가만히 두어도 이미 정렬된 상태이다. 
                       #따라서 이 단계에서 정렬을 마칠 수 있다.
array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]

for i in range(len(array)):
    min_index = i
    for j in range(i + 1, len(array)):
        if array[min_index] > array[j]:
            min_index = j
    array[i], array[min_index] = array[min_index], array[i]

print(array)

''' 결과 값
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
'''

선택 정렬의 시간 복잡도

선택 정렬은 N - 1번 만큼 가장 작은 수를 찾아서 맨 앞으로 보내야 한다. 또한 매번 가장 작은 수를 착기 위해서 비교 연산이 필요하다.

O(N^2)

선택 정렬은 기본 정렬 라이브러리를 포함해 다른 알고리즘과 비교했을 때 매우 비효율적이다.

삽입 정렬 (Insertion Sort)

특정한 데이터를 적절한 위치에 '삽입'한다는 의미이다. 더불어 삽입 정렬은 특정한 데이터가 적절한 위치에 들어가기 이전에, 그 앞까지의 데이터는 이미 정렬되어 있다고 가정한다. 정렬되어 있는 데이터 리스트에서 적절한 위치를 찾은 뒤에, 그 위치에 삽입된다는 점이 특징이다.

1. 7 (5) 9 0 3 1 6 2 4 8 #첫 번째 데이터 7은 그 자체로 정렬되어 있다고 판단하고, 두 번째 데이터인 5가 어떤 위치로 들어갈지 판단한다. 
                         #7의 왼쪽으로 들어가거나 혹은 오른쪽으로 들어가는 두 경우만 존재한다. 오름차순으로 정렬하고자 하므로 7의 왼쪽에 삽입한다.
2. 5 7 (9) 0 3 1 6 2 4 8 #9가 어떤 위치에 들어갈지 판단한다. 삽입될 수 있는 위치는 총 3가지이며 현재 9는 5와 7보다 크기 때문에 원래 자리 그대로 둔다.
3. 5 7 9 (0) 3 1 6 2 4 8 #0이 어떤 위치에 들어갈지 판단한다. 0은 5, 7, 9와 비교했을 때 가장 작기 때문에 첫 번째 위치에 삽입한다.
...
8. 0 1 2 3 5 6 7 9 (4) 8
9. 0 1 2 3 4 5 6 7 9 (8)
10. 0 1 2 3 4 5 6 7 8 9 #적절한 위치에 삽입하는 과정을 N - 1번 반복하게 되면 다음과 같이 모든 데이터가 정렬된 것을 확인할 수 있다.
array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]

for i in range(1, len(array)):
    for j in range(i, 0, -1):
        if array[j] < array[j - 1]:
            array[j], array[j - 1] = array[j - 1], array[j]
        else:
            break

print(array)

''' 결과 값
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
'''

삽입 정렬의 시간 복잡도

삽입 정렬의 시간 복잡도는 O(N^2)이다.
단, 삽입 정렬은 현재 리스트의 데이터가 거의 정렬되어 있는 상태라면 매우 빠르게 동작한다.
최선의 경우 O(N)의 시간 복잡도를 가진다.
보통 삽입 정렬은 비효율적이나 정렬이 거의 되어 있는 상황에서는 퀵 정렬 알고리즘보다 더 강력하다.

퀵 정렬 (Quick Sort)

퀵 정렬은 기준을 설정한 다음 큰 수와 작은 수를 교환한 후 리스트를 반으로 나누는 방식으로 동작한다.
퀵 정렬에는 Pivot이 사용되는데 큰 숫자와 작은 숫자를 교환할 때 기준을 피벗이라고 한다.

1. [5] (7) 9 0 3 1 6 2 (4) 8 #리스트의 첫 번째 데이터를 피벗으로 설정하므로 피벗은 5이다.
                             #이후에 왼쪽에서부터 5보다 큰 데이터를 선택하므로 7이 선택되고,
                             #오른쪽에서부터 5보다 작은 데이터를 선택하므로 4가 선택된다.
                             #그리고 이 두 데이터의 위치를 서로 변경한다.
2. [5] 4 (9) 0 3 1 6 (2) 7 8 #그 다음 피벗보다 큰 데이터와 작은 데이터를 각각 찾는다.
                             #찾은 뒤에는 두 값의 위치를 서로 변경하는데, 
                             #현재 9와 2가 선택되었으므로 이 두 데이터의 위치를 서로 변경한다.
3. [5] 4 2 0 3 (1) (6) 9 7 8 #현재 왼쪽에서부터 찾는 값과 오른쪽에서부터 찾는 값의 위치가 
                             #서로 엇갈린 것을 알 수 있다.
                             #이렇게 두 값이 엇갈린 경우에는 작은 데이터와 피벗의 위치를 
                             #서로 변경한다.
4. 1 4 2 0 3 [5] 6 9 7 8     #분할 완료

위와 같은 작업을 동일하게 반복해준다.
왼쪽 리스트와 오른쪽 리스트에서도 각각 피벗을 설정하여 동일한 방식으로 정렬을 수행하면 전체 리스트에 대하여 모두 정렬이 이루어질 것이다.

# 전통 퀵 정렬 
array = [5, 7, 9, 0, 3, 1, 6, 2, 4, 8]

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)

quick_sort(array, 0, len(array) - 1)
print(array)
# 파이썬스러운 퀵 정렬
array = [5, 7, 9, 0, 3, 1, 6, 2, 4, 8]

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)

print(quick_sort(array))

퀵 정렬의 시간 복잡도

퀵 정렬의 평균 시간 복잡도는 O(NlogN)이다. 단, 최악의 경우 시간 복잡도는 O(N^2)이다.
데이터가 무작위로 입력되는 경우 퀵 정렬은 빠르게 동작할 확률이 높다.

계수 정렬 (Count Sort)

계수 정렬 알고리즘은 특정한 조건이 부합할 때만 사용할 수 있지만 매우 빠른 정렬 알고리즘이다.
계수 정렬은 데이터의 크기 범위가 제한되어 정수 형태로 푠현할 수 있을 때만 사용할 수 있다.
일반적으로 가장 큰 데이터와 가장 작은 데이터의 차이가 1,000,000을 넘지 않을 때 효과적으로 사용할 수 있다.
계수 정렬은 일반적으로 별도의 리스트를 선언하고 그 안에 정렬에 대한 정보를 담는다는 특징이 있다.

초기 단계: 7 5 9 0 3 1 6 2 9 1 4 8 0 5 2
최대값 9 + 1의 크기를 가진 리스트를 선언한다.

1. 7을 선택하여 별도의 리스트 7번째 인덱스 값을 +1 해준다.
2. 5을 선택하여 별도의 리스트 5번째 인덱스 값을 +1 해준다.
...

결과적으로 위와 같이 리스트에는 각 데이터가 몇 번 등장했는지 그 횟수가 기록된다.

array = [7, 5, 9, 0, 3, 1, 6, 2, 9, 1, 4, 8, 0, 5, 2]

count = [0] * (max(array) + 1)

for arr in array:
    count[arr] += 1

for i in range(len(count)):
    for _ in range(count[i]):
        print(i, end = ' ')

계수 정렬의 시간 복잡도

모든 데이터가 양의 정수인 상황에서 데이터의 개수를 N, 데이터 중 최대 값의 크기를 K라고 할 때, 계수 정렬의 시간 복잡도는 O(N + K)이다.

계수 정렬의 공간 복잡도

계수 정렬은 때에 따라서 심각한 비효율성을 초래할 수 있다. 예를 들어 데이터가 0과 999,999, 단 2개만 존재한다고 하면, 리스트 크기가 100만 개가 되도록 선언해야한다. 따라서 항상 사용할 수 있는 정렬 알고리즘은 아니며, 동일한 값을 가지는 데이터가 여러 개 등장할 때 적합하다.
계수 정렬의 공간 복잡도는 O(N + K)이다.

Reference

  • 이것이 코딩 테스트다 with 파이썬
반응형