6강: 함수와 재귀
Python으로 “정렬 경주” 패턴을 만들면서 배워요:
- 함수, 그리고 함수를 값으로 넘기기
- 재귀: 더 작은 부분에 대해 자기 자신을 부르는 함수
- 딕셔너리에 담아 두는 카운터
- 결과를 확인하는 assert
- 일이 늘어나는 방식: n² 대 n·log n
먼저 보기
클릭하면 YouTube에서 재생돼요.
"""Sorting race: four algorithms on the same shuffled list, counting their comparisons.
Run: python sorting-race.py (needs: pip install numpy matplotlib)
"""
import math
import numpy as np
import matplotlib.pyplot as plt
plt.style.use("dark_background")
n = 100
data = list(np.random.default_rng().permutation(n))
count = {"comparisons": 0}
def less(a, b): # every algorithm compares only through this, so we can count
count["comparisons"] += 1
return a < b
def bubble(xs):
xs = xs[:]
for end in range(len(xs) - 1, 0, -1):
for i in range(end):
if less(xs[i + 1], xs[i]):
xs[i], xs[i + 1] = xs[i + 1], xs[i]
return xs
def insertion(xs):
xs = xs[:]
for i in range(1, len(xs)):
j = i
while j > 0 and less(xs[j], xs[j - 1]):
xs[j], xs[j - 1] = xs[j - 1], xs[j]
j -= 1
return xs
def merge(xs):
if len(xs) < 2:
return xs
left, right = merge(xs[: len(xs) // 2]), merge(xs[len(xs) // 2 :])
out = []
while left and right:
out.append(left.pop(0) if not less(right[0], left[0]) else right.pop(0))
return out + left + right
def quick(xs):
if len(xs) < 2:
return xs
pivot, rest = xs[0], xs[1:]
smaller = [x for x in rest if less(x, pivot)]
return quick(smaller) + [pivot] + quick([x for x in rest if x not in smaller])
results = {}
for name, sort in [("bubble", bubble), ("insertion", insertion), ("merge", merge), ("quick", quick)]:
count["comparisons"] = 0
assert sort(data) == sorted(data)
results[name] = count["comparisons"]
print(results, "| at least log2(n!) =", math.ceil(math.log2(math.factorial(n))))
plt.figure(figsize=(7, 4))
plt.bar(list(results), list(results.values()), color=["#f0abfc", "#c4b5fd", "#5eead4", "#5eead4"])
plt.axhline(math.log2(math.factorial(n)), color="white", linestyle="--", label="log2(n!): no comparison sort does better")
plt.ylabel(f"comparisons for n = {n}")
plt.legend()
plt.show()
# Try: n = 1000 (bubble sort needs about n*n/2, merge sort about n*log2(n))
이 컴퓨터에 Python이 없나요? 노트북으로 같은 스크립트와 연습 문제, 정답을 브라우저의 Google Colab에서 실행할 수 있어요. 노트북은 영어로 되어 있어요.
한 줄씩
def less(a, b):모든 알고리즘이 이 함수 하나로 비교하므로, 비교 횟수를 한곳에서 셀 수 있어요.
count["comparisons"] += 1딕셔너리가 횟수를 담고 있어서, 함수가 global 키워드 없이 그 값을 바꿀 수 있어요.
def merge(xs):병합 정렬이에요. 두 절반을 각각 정렬한 다음, 언제나 맨 앞 항목 중 더 작은 것을 가져오며 정렬된 두 절반을 합쳐요.
left, right = merge(xs[: len(xs) // 2]), merge(xs[len(xs) // 2 :])재귀예요. merge는 각 절반에 대해 자기 자신을 부르고, 부분의 항목이 두 개보다 적어질 때까지 계속해요.
assert sort(data) == sorted(data)assert는 정렬이 순서를 틀리면 스크립트를 멈춰요. 프로그램 안에 넣어 둔 테스트예요.
math.ceil(math.log2(math.factorial(n)))하한이에요. 가능한 순서의 수 n!의 log₂를 올림한 값이에요.
직접 해 보기
n = 100일 때 비교 횟수가 가장 적은 정렬은 무엇이고, 하한에 얼마나 가까울까요?
힌트
스크립트를 실행해 보세요. 횟수와 하한을 출력해요.
정답
병합 정렬로, 하한 525에 비해 약 540번이에요. 버블 정렬은 정확히 4,950번, 곧 100 · 99 ÷ 2번이 필요해요.
이 버블 정렬은 왜 언제나 n·(n − 1) ÷ 2번 비교할까요?
힌트
두 반복문을 살펴보세요.
정답
이 버전은 두 반복문을 언제나 끝까지 돌아요: 순서가 어떻든 (n − 1) + (n − 2) + … + 1 = n·(n − 1) ÷ 2번 비교해요.
이미 정렬된 리스트를 퀵 정렬에 줘 보세요. 횟수는 어떻게 될까요?
힌트
data = list(range(n)).
정답
4,950번으로 뛰어올라 버블 정렬만큼 나빠져요. 첫 항목을 피벗으로 쓰면 나눌 때마다 한쪽이 비기 때문이에요.
퀴즈
플래시카드
앞면을 읽고, 뒷면의 내용을 스스로 말해 본 다음 카드를 펼쳐 보세요.
재귀
문제의 더 작은 부분에 대해 자기 자신을 불러서 문제를 푸는 함수.
assert condition
조건이 False이면 오류를 내며 프로그램을 멈춰요.
log₂(n!)
n!가지 순서를 구별할 수 있는 예/아니요 비교의 최소 횟수: 모든 비교 정렬의 한계.
n² 대 n·log n
항목이 1,000개일 때: 약 500,000단계 대 약 10,000단계.
함수 넘기기
for name, sort in [("merge", merge), …]: 함수도 수처럼 값이에요.