개념

가장 작은 값을 찾아 맨 앞부터 차례대로 채워나가는 정렬

남은 부분에서 최솟값을 선택(Select)하여 정렬


시간복잡도

케이스시간복잡도
최선O(n²)
평균O(n²)
최악O(n²)
공간복잡도O(1)

특징

장점

  • 구현 간단: 이해하기 쉬운 정렬
  • 제자리 정렬: 추가 메모리 불필요
  • 교환 횟수 적음: 최대 n-1번의 교환

단점

  • 매우 느림: O(n²) 시간복잡도
  • 불안정 정렬: 같은 값의 순서가 바뀔 수 있음
  • 비효율적: 이미 정렬되어 있어도 O(n²)

구현 방법

기본 구현

def selection_sort(arr):
    n = len(arr)
 
    for i in range(n - 1):
        # 최솟값의 인덱스 찾기
        min_idx = i
 
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
 
        # 최솟값을 현재 위치와 교환
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
 
    return arr
 
# 테스트
print(selection_sort([64, 25, 12, 22, 11]))
# [11, 12, 22, 25, 64]

내림차순 구현

def selection_sort_desc(arr):
    n = len(arr)
 
    for i in range(n - 1):
        # 최댓값의 인덱스 찾기
        max_idx = i
 
        for j in range(i + 1, n):
            if arr[j] > arr[max_idx]:
                max_idx = j
 
        # 최댓값을 현재 위치와 교환
        arr[i], arr[max_idx] = arr[max_idx], arr[i]
 
    return arr
 
# 테스트
print(selection_sort_desc([64, 25, 12, 22, 11]))
# [64, 25, 22, 12, 11]

동작 과정

초기: [64, 25, 12, 22, 11]

1회전: (최솟값 11 찾기)
[11, 25, 12, 22, 64]  ← 11과 64 교환

2회전: (최솟값 12 찾기)
[11, 12, 25, 22, 64]  ← 12와 25 교환

3회전: (최솟값 22 찾기)
[11, 12, 22, 25, 64]  ← 22와 25 교환

4회전: (최솟값 25 찾기)
[11, 12, 22, 25, 64]  ← 이미 정렬됨

완료: [11, 12, 22, 25, 64]

템플릿

def selection_sort_template(arr):
    n = len(arr)
 
    # i: 정렬된 부분의 끝 (0부터 n-2까지)
    for i in range(n - 1):
        # 최솟값의 인덱스 초기화
        min_idx = i
 
        # j: 미정렬 부분에서 최솟값 찾기
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
 
        # 찾은 최솟값을 정렬된 부분 끝과 교환
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
 
    return arr

핵심 요약

개념설명
원리최솟값 선택 후 앞으로 이동
시간복잡도O(n²) - 항상 동일
공간복잡도O(1)
안정성불안정 정렬
교환 횟수최대 n-1번

다른 정렬과 비교

정렬시간복잡도 (평균)공간복잡도안정성교환 횟수
선택 정렬O(n²)O(1)불안정최대 n-1
버블 정렬O(n²)O(1)안정최대 n²/2
삽입 정렬O(n²)O(1)안정최대 n²/2
병합 정렬O(n log n)O(n)안정-
퀵 정렬O(n log n)O(log n)불안정-

주의사항

1. 불안정 정렬

# 같은 값의 순서가 바뀔 수 있음
arr = [(5, 'a'), (3, 'b'), (5, 'c'), (2, 'd')]
# 정렬 후: (5, 'c')가 (5, 'a')보다 앞에 올 수 있음

2. 최적화의 한계

# 이미 정렬되어 있어도 O(n²)
# 조기 종료 불가능
# 버블 정렬처럼 최선의 경우 O(n)으로 개선 안 됨

3. 사용 권장 사항

# 교환 비용이 매우 클 때만 고려
# 예: 큰 객체의 물리적 이동이 비쌀 때
# 일반적으로는 삽입 정렬이나 다른 정렬 사용 권장

자주 나오는 문제 유형

1. K번째 작은 수 찾기

문제: 배열에서 K번째로 작은 수 찾기

def find_kth_smallest(arr, k):
    n = len(arr)
 
    # k-1번만 선택 정렬 수행
    for i in range(k):
        min_idx = i
 
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
 
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
 
    return arr[k - 1]
 
# 테스트
print(find_kth_smallest([7, 10, 4, 3, 20, 15], 3))  # 7
 
# 시간복잡도: O(k × n)

핵심: K번만 선택 정렬을 수행하면 됨


2. 선택 정렬 최적화 (양방향 선택)

문제: 최솟값과 최댓값을 동시에 찾아 양쪽 끝에 배치

def selection_sort_bidirectional(arr):
    left = 0
    right = len(arr) - 1
 
    while left < right:
        min_idx = left
        max_idx = right
 
        # 최솟값과 최댓값 동시에 찾기
        for i in range(left, right + 1):
            if arr[i] < arr[min_idx]:
                min_idx = i
            if arr[i] > arr[max_idx]:
                max_idx = i
 
        # 최솟값을 왼쪽 끝으로
        arr[left], arr[min_idx] = arr[min_idx], arr[left]
 
        # max_idx가 left를 가리키고 있었다면 갱신
        if max_idx == left:
            max_idx = min_idx
 
        # 최댓값을 오른쪽 끝으로
        arr[right], arr[max_idx] = arr[max_idx], arr[right]
 
        left += 1
        right -= 1
 
    return arr
 
# 테스트
print(selection_sort_bidirectional([64, 25, 12, 22, 11]))
# [11, 12, 22, 25, 64]
 
# 시간복잡도: O(n²) - 비교 횟수는 절반으로 감소

핵심: 한 번의 순회로 최솟값과 최댓값 모두 처리


3. 교환 횟수 세기

문제: 선택 정렬 중 발생하는 교환 횟수 세기

def selection_sort_count_swaps(arr):
    n = len(arr)
    swap_count = 0
 
    for i in range(n - 1):
        min_idx = i
 
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
 
        # 교환이 필요한 경우에만
        if min_idx != i:
            arr[i], arr[min_idx] = arr[min_idx], arr[i]
            swap_count += 1
 
    return arr, swap_count
 
# 테스트
result, swaps = selection_sort_count_swaps([64, 25, 12, 22, 11])
print(f"정렬 결과: {result}")
print(f"교환 횟수: {swaps}")  # 4
 
# 최대 교환 횟수: n-1

핵심: 각 회전마다 최대 1번의 교환만 발생


추천 연습 문제

기초


선택 정렬 vs 버블 정렬

특성선택 정렬버블 정렬
교환 횟수최대 n-1번최대 n²/2번
비교 횟수n(n-1)/2번n(n-1)/2번
안정성불안정안정
최선의 경우O(n²)O(n) (최적화 시)
사용 시기교환 비용이 클 때거의 정렬된 경우

언제 사용할까?

사용 가능

  • 교환 비용이 매우 클 때: 큰 객체의 물리적 이동이 비쌀 때
  • 메모리가 제한적일 때: O(1) 공간복잡도
  • 작은 데이터: n < 10 정도

사용 불가

  • 일반적인 경우: 삽입 정렬이 더 나음
  • 거의 정렬된 경우: 삽입 정렬이 O(n)
  • 대용량 데이터: 병합 정렬, 퀵 정렬 사용
  • 안정 정렬 필요 시: 병합 정렬, 삽입 정렬 사용

결론: 교환 비용이 매우 클 때만 고려, 그 외엔 다른 정렬 사용!