알고리즘이 문제를 푸는 방법이라고 했는데, 그러면 정렬 알고리즘은 뭘 정렬하기 위한 방법이겠지? 네, 맞습니다. 이것도 여러가지가 있는데 대표적인 것 다섯가지만 일단 알아보자.
코드와 알고리즘 관련 설명은
https://velog.io/@jguuun/%EC%A0%95%EB%A0%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98
여기서 볼 수 있다.
버블 정렬
a = [80, 58, 66, 100, 71, 39, 89, 67, 25, 9]
def bubble_sort(array):
n = len(array)
for i in range(n):
for j in range(n - i - 1):
if array[j] > array[j + 1]:
array[j], array[j + 1] = array[j + 1], array[j]
print(array)
bubble_sort(a)
[58, 80, 66, 100, 71, 39, 89, 67, 25, 9]
[58, 66, 80, 100, 71, 39, 89, 67, 25, 9]
[58, 66, 80, 100, 71, 39, 89, 67, 25, 9]
[58, 66, 80, 71, 100, 39, 89, 67, 25, 9]
[58, 66, 80, 71, 39, 100, 89, 67, 25, 9]
[58, 66, 80, 71, 39, 89, 100, 67, 25, 9]
[58, 66, 80, 71, 39, 89, 67, 100, 25, 9]
[58, 66, 80, 71, 39, 89, 67, 25, 100, 9]
[58, 66, 80, 71, 39, 89, 67, 25, 9, 100]
[58, 66, 80, 71, 39, 89, 67, 25, 9, 100]
[58, 66, 80, 71, 39, 89, 67, 25, 9, 100]
[58, 66, 71, 80, 39, 89, 67, 25, 9, 100]
[58, 66, 71, 39, 80, 89, 67, 25, 9, 100]
[58, 66, 71, 39, 80, 89, 67, 25, 9, 100]
[58, 66, 71, 39, 80, 67, 89, 25, 9, 100]
[58, 66, 71, 39, 80, 67, 25, 89, 9, 100]
[58, 66, 71, 39, 80, 67, 25, 9, 89, 100]
[58, 66, 71, 39, 80, 67, 25, 9, 89, 100]
[58, 66, 71, 39, 80, 67, 25, 9, 89, 100]
[58, 66, 39, 71, 80, 67, 25, 9, 89, 100]
[58, 66, 39, 71, 80, 67, 25, 9, 89, 100]
[58, 66, 39, 71, 67, 80, 25, 9, 89, 100]
[58, 66, 39, 71, 67, 25, 80, 9, 89, 100]
[58, 66, 39, 71, 67, 25, 9, 80, 89, 100]
[58, 66, 39, 71, 67, 25, 9, 80, 89, 100]
[58, 39, 66, 71, 67, 25, 9, 80, 89, 100]
[58, 39, 66, 71, 67, 25, 9, 80, 89, 100]
[58, 39, 66, 67, 71, 25, 9, 80, 89, 100]
[58, 39, 66, 67, 25, 71, 9, 80, 89, 100]
[58, 39, 66, 67, 25, 9, 71, 80, 89, 100]
[39, 58, 66, 67, 25, 9, 71, 80, 89, 100]
[39, 58, 66, 67, 25, 9, 71, 80, 89, 100]
[39, 58, 66, 67, 25, 9, 71, 80, 89, 100]
[39, 58, 66, 25, 67, 9, 71, 80, 89, 100]
[39, 58, 66, 25, 9, 67, 71, 80, 89, 100]
[39, 58, 66, 25, 9, 67, 71, 80, 89, 100]
[39, 58, 66, 25, 9, 67, 71, 80, 89, 100]
[39, 58, 25, 66, 9, 67, 71, 80, 89, 100]
[39, 58, 25, 9, 66, 67, 71, 80, 89, 100]
[39, 58, 25, 9, 66, 67, 71, 80, 89, 100]
[39, 25, 58, 9, 66, 67, 71, 80, 89, 100]
[39, 25, 9, 58, 66, 67, 71, 80, 89, 100]
[25, 39, 9, 58, 66, 67, 71, 80, 89, 100]
[25, 9, 39, 58, 66, 67, 71, 80, 89, 100]
[9, 25, 39, 58, 66, 67, 71, 80, 89, 100]
인접한 두 수를 비교해가면서 정렬하는 방식. 비유하자면 얘가 얘보다 출석번호가 큰가를 일일이 확인하고 출석번호가 더 크면 뒤로 보낸다.
보시다시피 코드는 심플하지만, 실행 결과를 보면 하나하나 대조해가면서 얘가 얘보다 크면 뒤로 빼는 방식이라 효율면에서는 씁 에반데 소리가 절로 나온다.
선택 정렬
a = [80, 58, 66, 100, 71, 39, 89, 67, 25, 9]
def selection_sort(array):
n = len(array)
for i in range(n):
min_index = i
for j in range(i + 1, n):
if array[j] < array[min_index]:
min_index = j
array[i], array[min_index] = array[min_index], array[i]
print(array)
selection_sort(a)
[9, 58, 66, 100, 71, 39, 89, 67, 25, 80]
[9, 25, 66, 100, 71, 39, 89, 67, 58, 80]
[9, 25, 39, 100, 71, 66, 89, 67, 58, 80]
[9, 25, 39, 58, 71, 66, 89, 67, 100, 80]
[9, 25, 39, 58, 66, 71, 89, 67, 100, 80]
[9, 25, 39, 58, 66, 67, 89, 71, 100, 80]
[9, 25, 39, 58, 66, 67, 71, 89, 100, 80]
[9, 25, 39, 58, 66, 67, 71, 80, 100, 89]
[9, 25, 39, 58, 66, 67, 71, 80, 89, 100]
[9, 25, 39, 58, 66, 67, 71, 80, 89, 100]
배열을 쓱 보면서 가장 작은 값을 앞으로 보낸다. 앞에서부터 뒤로 쭉 둘러보면서 1번을 맨앞으로, 그 다음 2번을 맨 앞으로, 3번을 맨 앞으로… 이런 식으로 진행한다. 버블 정렬에 비해 과정이 짧아보이는데 기분탓…은 아님.
삽입 정렬
a = [80, 58, 66, 100, 71, 39, 89, 67, 25, 9]
def insert_sort(array):
n = len(array)
for i in range(n):
min_index = i
for j in range(i, 0, -1):
if array[j - 1] > array[j]:
array[j - 1], array[j] = array[j], array[j - 1]
print(array)
insert_sort(a)
[80, 58, 66, 100, 71, 39, 89, 67, 25, 9]
[58, 80, 66, 100, 71, 39, 89, 67, 25, 9]
[58, 66, 80, 100, 71, 39, 89, 67, 25, 9]
[58, 66, 80, 100, 71, 39, 89, 67, 25, 9]
[58, 66, 71, 80, 100, 39, 89, 67, 25, 9]
[39, 58, 66, 71, 80, 100, 89, 67, 25, 9]
[39, 58, 66, 71, 80, 89, 100, 67, 25, 9]
[39, 58, 66, 67, 71, 80, 89, 100, 25, 9]
[25, 39, 58, 66, 67, 71, 80, 89, 100, 9]
[9, 25, 39, 58, 66, 67, 71, 80, 89, 100]
데이터의 사이를 비우고 그 사이로 순서에 맞는 데이터를 넣는 정렬 구조. 출석번호 2번과 4번의 사이를 벌리고 그 사이로 3번을 넣는다. 보통 사람들이 책이나 서류철같은 거 정리할 때 이런 식으로 한다.
병합 정렬
a = [80, 58, 66, 100, 71, 39, 89, 67, 25, 9]
def merge_sort(array):
if len(array) < 2:
return array
mid = len(array) // 2
low_arr = merge_sort(array[:mid])
high_arr = merge_sort(array[mid:])
# 일단 짼다
merged_arr = []
l = h = 0
while l < len(low_arr) and h < len(high_arr):
if low_arr[l] < high_arr[h]:
merged_arr.append(low_arr[l])
l += 1
else:
merged_arr.append(high_arr[h])
h += 1
merged_arr += low_arr[l:]
merged_arr += high_arr[h:]
print(merged_arr)
return merged_arr
# 그리고 비교한다
merge_sort(a)
[58, 80]
[71, 100]
[66, 71, 100]
[58, 66, 71, 80, 100]
[39, 89]
[9, 25]
[9, 25, 67]
[9, 25, 39, 67, 89]
[9, 25, 39, 58, 66, 67, 71, 80, 89, 100]
폰 노이만이 고안한 알고리즘. 이 양반에 대한 이야기는 아래를 참고하시고… (야공만 재밌음)
https://www.facebook.com/engineertoon/posts/579547758898750
이게 그래서 뭐냐면… 배열의 길이가 0이나 1이 될 때까지 쪼개고 비교해서 합한다. 그러니까
- 이 배열을 [80, 58, 66, 100, 71] [39, 89, 67, 25, 9]로 나누고
- 또 배열을 [80, 58] [66, 100, 71] [39, 89] [67, 25, 9]로 나누고
- 계속 나눠서 배열의 길이가 1이 될 때까지 나눈다. (홀수 배열은 아마도 [1] [2, 3] 이런 식인 듯)
- 나눈 배열에 있는 숫자를 비교해서 작은 쪽을 앞으로 빼고 다시 합친다. (if문이 그래서 있음)
대충 이런 식.
퀵 정렬
a = [80, 58, 66, 100, 71, 39, 89, 67, 25, 9]
def quick_sort(array):
if len(array) <= 1:
return array
pivot = len(array) // 2
front_arr, pivot_arr, back_arr = [], [], []
for value in array:
if value < array[pivot]:
front_arr.append(value)
elif value > array[pivot]:
back_arr.append(value)
else:
pivot_arr.append(value)
print(front_arr, pivot_arr, back_arr)
return quick_sort(front_arr) + quick_sort(pivot_arr) + quick_sort(back_arr)
quick_sort(a)
[25, 9] [39] [80, 58, 66, 100, 71, 89, 67]
[] [9] [25]
[80, 58, 66, 71, 89, 67] [100] []
[58, 66, 67] [71] [80, 89]
[58] [66] [67]
[80] [89] []
[9, 25, 39, 58, 66, 67, 71, 80, 89, 100]
로직 잘못 짜면 성능을 조지긴 한데 아무튼… 한 반에 25명이 있고 출석번호 순으로 줄을 세울 때 퀵 알고리즘은 기준점을 두고 그걸 중심으로 분할한다. 이 코드에서는 배열의 가운데(여기서는 길이가 10인 배열의 5번 값)를 기준으로 해서 정렬했다. 이것도 일종의 분할 알고리즘이라 피벗을 중심으로 앞을 정렬하고 뒤도 기준점을 잡아서 정렬한 것을 볼 수 있다.
알고리즘의 소요시간 비교해보기
버블 알고리즘이 개똥이라는데 진짜일까? 확인해보자. 일단 확인하기 위한 배열이 하나 필요한데, 시간복잡도가 체감되는 건 ‘데이터가 많아질때’이다. 그래서 배열의 길이를 10개 뭐 이렇게 스몰스케일로 안 하고
[793, 27, 646, 705, 964, 814, 804, 300, 942, 614, 765, 790, 739, 191, 474, 503, 887, 908, 476, 733, 964, 564, 579, 22, 395, 645, 713, 763, 830, 909, 206, 197, 540, 489, 124, 696, 957, 882, 759, 557, 198, 987, 424, 777, 111, 893, 813, 329, 570, 707, 226, 423, 246, 86, 878, 192, 421, 559, 871, 62, 136, 768, 831, 813, 477, 714, 805, 279, 186, 281, 404, 252, 548, 546, 594, 1, 627, 351, 721, 11, 637, 386, 657, 907, 35, 753, 827, 227, 116, 745, 586, 105, 4, 444, 488, 599, 404, 3, 291, 634, 848, 500, 260, 273, 575, 549, 713, 166, 109, 930, 761, 997, 21, 333, 160, 569, 927, 210, 315, 592, 330, 305, 839, 931, 822, 957, 738, 552, 478, 886, 809, 855, 579, 965, 661, 572, 219, 878, 830, 542, 82, 401, 30, 276, 664, 596, 616, 500, 74, 382, 191, 947, 521, 715, 575, 65, 110, 722, 751, 710, 953, 229, 586, 852, 447, 604, 529, 82, 760, 755, 167, 609, 152, 229, 276, 834, 84, 411, 817, 988, 572, 318, 192, 292, 438, 708, 450, 404, 204, 348, 387, 401, 88, 431, 246, 131, 55, 816, 211, 726]
길이 200짜리 배열을 준비했다. (물론 난수로 만들었음) 그리고 하나 더 있는데… 코드가 구동되는 시간을 뭐 스톱워치로 잴 수는 없잖음?
import random, sys, time
start = time.time()
N = int(sys.stdin.readline())
def random_array(n):
a = []
for i in range(n):
i = random.randrange(1,101)
a.append(i)
for j in a:
if i == j:
i = random.randrange(1,101)
else:
continue
return a
print(random_array(N))
print('소요시간: ',round(time.time() - start,3))
import time 하고 시작시간 지정하고 완료 후의 시간에서 빼서 소요시간을 계산할 수 있다. 그리고 결과는…
- 버블 정렬 소요시간: 7.392
- 선택 정렬 소요시간: 0.075
- 삽입 정렬 소요시간: 0.08
- 병합 정렬 소요시간: 0.003
- 퀵 정렬 소요시간: 0.000118 (3자리 했더니 안나와서 자리수 늘렸음)
버블 정렬이 진짜 느리다. 그래서 진짜 막 이거 아님 안될것같다… 그런거 아니면 버블 정렬은 지양하는 게 좋다고. 버블 정렬이 압도적이라 그렇지 2, 3번이 전체적으로 그렇게 효율이 좋지 않은데… 왜 그럴까? 병합이랑 퀵은 왜 그렇게 빠를까? 병합 정렬과 퀵 정렬은 배열을 ‘분할’해서 정렬한다. 출석 번호 정렬로 치자면 위에 3개는 한 반 전체를 정렬하는거고, 아래 두 개는 어떤 기준으로 한 반의 학생들을 그룹으로 나눠서 정렬한다.
아니 이렇게까지 해야 함?
근데 이걸 왜 정렬까지 해야 하나요? 그것은 간단하다. 데이터를 어떤 기준으로든 정렬해두면, 이진 탐색 알고리즘을 이용할 수 있다. 예? 그게 뭔 소리유? 예를 들어보자.
평소처럼 실험을 하던 제육쌈밥군(R로 스탠다드 커브 그리기 참고). 그러던 어느 날, 교수님께 퀘스트가 들어왔다.
“여기 있는 실험노트 중, 2015년 이전에 작성된 노트를 내 방으로 옮겨줘. “
제육쌈밥군이 있는 실험실은 교수님의 인기가 좋아 대학원을 거쳐 간 사람들이 꽤 있었고, 노트는 어림잡아 3~400권 정도는 되는 것 같았다.
- 정렬이 없을 때: 일단 제육쌈밥군은 최근에 실험실에 들어온 선배들의 노트를 제외하고, 책꽂이에 꽂혀 있는 실험노트를 전부 꺼냈다. 그리고 실험노트 앞쪽에 적힌 날짜를 보면서 일일이 연도를 확인하면서 2015년 이전에 작성된 노트를 골라냈고, 교수님 사무실로 옮겼다.
- 정렬이 있을 때(전체 정렬): 제육쌈밥군은 노트 뭉치를 가져와서 한데 모은 다음, 표지에 쓰인 연도순으로 정렬했다. 한참동안 노트를 정리한 제육쌈밥은 2015년 이전에 작성된 노트를 교수님 사무실로 옮겼다.
- 정렬이 있을 때(분할 정렬): 제육쌈밥군은 노트 뭉치를 가져와서 책꽂이 칸별로 쌓았다. 그리고 노트 더미에서 노트를 꺼내 앞표지를 확인하면서 노트를 정리한 제육쌈밥은 정리를 마치고 2015년 이전에 작성된 노트를 교수님 사무실로 옮겼다.
세 가지 케이스를 얼핏 들어서는 별로 차이가 없을 것 같지만, 노트를 일단 정렬하게 되면 2014년 12월에 작성된 마지막 노트에 표시하고 그것만 잘라서 정리하면 된다. 정리하는데 시간은 들겠지만, 정리하고 나서 그냥 기준에 따라 뚝 자르면 되는 것.
Reply