cv.cm

코딩 · 알고리즘

선택 정렬 알고리즘 원리, 코드, 시간 복잡도

6분 분량

핵심 답변

선택 정렬은 리스트를 왼쪽의 정렬된 부분과 오른쪽의 정렬되지 않은 부분으로 나눕니다. 매 단계마다 정렬되지 않은 부분에서 가장 작은 요소를 찾아 그 부분의 첫 위치와 교환합니다. 비교는 항상 약 n²/2번 하므로 어떤 경우에도 O(n²)이지만, 교환은 최대 n − 1번이고 추가 공간은 O(1)입니다. 일반적인 배열 버전은 안정 정렬이 아닙니다.

줄 정렬 열기

쉬운 말로 설명하는 원리

리스트 전체를 훑어 가장 작은 요소를 찾습니다. 그것을 인덱스 0의 요소와 교환하면 이제 인덱스 0은 확정입니다. 인덱스 1부터 나머지를 훑어 가장 작은 요소를 찾아 인덱스 1과 교환합니다. 요소가 하나만 남을 때까지 계속하면, 남은 것은 가장 큰 요소입니다.

버블 정렬이나 삽입 정렬과 달리 선택 정렬은 입력이 이미 정렬되어 있어도 이득이 없습니다. 최솟값을 찾았다고 확신하려면 남은 요소를 모두 훑어야 하기 때문입니다.

단계별 예제

[64, 25, 12, 22, 11]을 정렬해 봅시다. 막대 앞 부분은 정렬이 끝나 확정된 부분입니다.

단계정렬되지 않은 부분찾은 최솟값교환단계 후 리스트
164, 25, 12, 22, 11인덱스 4의 11인덱스 0 ↔ 4[11 | 25, 12, 22, 64]
225, 12, 22, 64인덱스 2의 12인덱스 1 ↔ 2[11, 12 | 25, 22, 64]
325, 22, 64인덱스 3의 22인덱스 2 ↔ 3[11, 12, 22 | 25, 64]
425, 64인덱스 3의 25필요 없음[11, 12, 22, 25 | 64]

요소 5개에 4단계이며, 비교는 4 + 3 + 2 + 1 = 10번, 실제 교환은 3번입니다.

파이썬 구현

def selection_sort(items):
    """Sort a list in place, ascending. Returns the same list."""
    n = len(items)
    for i in range(n - 1):
        smallest = i
        for j in range(i + 1, n):
            if items[j] < items[smallest]:
                smallest = j
        if smallest != i:
            items[i], items[smallest] = items[smallest], items[i]
    return items
print(selection_sort([64, 25, 12, 22, 11]))   # [11, 12, 22, 25, 64]
print(selection_sort([2, 1]))                 # [1, 2]

마지막 남은 요소는 자동으로 제자리에 있으므로 바깥 반복문은 n − 1번 실행됩니다. 안쪽 반복문은 인덱스만 기록하고, 교환은 바깥 단계마다 한 번만 일어납니다.

시간 복잡도와 공간 복잡도

측정 항목값이유
비교 횟수, 모든 경우n(n − 1)/2단계마다 정렬되지 않은 부분 전체를 훑음
시간, 최선 / 평균 / 최악O(n²)훑는 작업은 순서와 무관
교환 횟수, 최악n − 1단계마다 최대 한 번 교환
추가 공간O(1)제자리 정렬, 인덱스 변수 하나

쓰기 횟수가 적다는 점이 선택 정렬의 유일한 실질적 강점입니다. 일부 플래시 메모리처럼 메모리에 쓰는 비용이 읽는 비용보다 훨씬 크거나 큰 레코드를 옮겨야 할 때는 쓰기를 줄이는 것이 중요할 수 있습니다. 사이클 정렬(cycle sort)은 이를 더 밀어붙여 각 요소를 최대 한 번만 씁니다.

안정성: 선택 정렬이 안정 정렬이 아닌 이유

안정 정렬은 같은 값의 요소가 원래의 상대적 순서를 유지하는 정렬입니다. 선택 정렬은 멀리 떨어진 요소를 교환하므로, 같은 값을 가진 다른 요소를 뛰어넘을 수 있습니다.

def selection_sort_by_key(items, key):
    n = len(items)
    for i in range(n - 1):
        m = min(range(i, n), key=lambda j: key(items[j]))
        items[i], items[m] = items[m], items[i]
    return items
cards = [("4", "hearts"), ("4", "spades"), ("2", "clubs")]
print(selection_sort_by_key(cards, key=lambda c: c[0]))
# [('2', 'clubs'), ('4', 'spades'), ('4', 'hearts')]  order of the 4s changed

2가 하트 4와 교환되면서 하트 4가 스페이드 4 뒤로 옮겨졌습니다. 안정 버전도 있습니다. 교환하는 대신 최솟값을 빼내 위치 i에 끼워 넣고 나머지를 오른쪽으로 밀면 순서는 유지되지만 단계마다 쓰기가 O(n)번 듭니다.

선택 정렬과 다른 정렬 비교

알고리즘최선평균최악추가 공간안정 정렬
선택 정렬O(n²)O(n²)O(n²)O(1)아니요
버블 정렬O(n)O(n²)O(n²)O(1)예
삽입 정렬O(n)O(n²)O(n²)O(1)예
힙 정렬O(n log n)O(n log n)O(n log n)O(1)아니요
병합 정렬O(n log n)O(n log n)O(n log n)O(n)예
퀵 정렬O(n log n)O(n log n)O(n²)O(log n)아니요

힙 정렬은 더 나은 자료구조를 쓰는 선택 정렬입니다. 남은 요소 중 가장 큰 것을 반복해서 선택하는 점은 같지만, O(n) 탐색 대신 이진 힙으로 O(log n)에 찾으므로 전체가 O(n log n)이 됩니다.

선택 정렬을 쓰는 경우

실제 파이썬 코드에서는 내장 sorted()나 list.sort()를 쓰세요. 선택 정렬은 교육용 예제로, 제약이 있는 환경의 아주 작은 고정 크기 배열에, 쓰기 횟수를 최소화해야 할 때 알아 둘 가치가 있습니다. 시험과 면접에서 자주 나오며, 보통 단계를 추적하고, 비교와 교환 횟수를 세고, 안정 정렬이 아닌 이유를 설명하라는 형태입니다.

자주 묻는 질문

선택 정렬은 안정 정렬인가요?

표준 제자리 배열 버전은 안정 정렬이 아닙니다. 멀리 떨어진 요소와 교환하면 같은 값의 요소를 지나쳐 버릴 수 있기 때문입니다. 교환 대신 삽입하는 버전은 안정적이지만 쓰기가 더 많습니다.

선택 정렬의 시간 복잡도는 얼마인가요?

최선, 평균, 최악 모두 O(n²)입니다. 입력 순서와 관계없이 항상 n(n − 1)/2번 비교합니다.

선택 정렬은 교환을 몇 번 하나요?

바깥 단계마다 한 번씩, 최대 n − 1번입니다. O(n²)번의 교환이나 이동이 일어날 수 있는 버블 정렬이나 삽입 정렬보다 적습니다.

선택 정렬과 삽입 정렬은 무엇이 다른가요?

선택 정렬은 정렬되지 않은 부분에서 최솟값을 골라 정렬된 부분 뒤에 붙입니다. 삽입 정렬은 다음 요소를 정렬된 부분의 알맞은 위치에 끼워 넣습니다. 삽입 정렬은 안정 정렬이며 최선의 경우가 O(n)이지만, 선택 정렬은 둘 다 아닙니다.

코딩 튜토리얼 전체