선택정렬의 비교 비용과 스왑 최소화 특성

선택정렬의 최소값 선택 방식, 시간 복잡도, 제자리 정렬과 안정적 시프트 변형을 코드와 함께 정리한다.

2026-08-14 · 최초 발행 2024-04-29

최소값을 골라 정렬 구간을 넓히는 방식

선택정렬은 아직 정렬하지 않은 구간에서 최소값을 찾고, 이를 정렬이 끝난 구간의 다음 자리로 옮기는 비교 기반 알고리즘이다. k번째 단계에서는 인덱스 [k..n-1]에서 최소값의 위치를 찾은 뒤 A[k]와 교환한다. 이 과정을 거치면 정렬 완료 구간은 [0..k]까지 늘어난다.

루프가 시작될 때마다 [0..k-1]은 오름차순으로 정렬되어 있으며, 배열 전체에서 가장 작은 k개 원소를 포함한다는 불변식을 유지한다.

비교 횟수는 약 n(n-1)/2이므로 최선·평균·최악 모두 O(n^2)이다. 반면 교환은 최대 n-1회, 즉 O(n)에 그친다. 별도 버퍼 없이 동작하는 제자리(in-place) 정렬이며 추가 메모리는 O(1)이다.

기본적인 스왑 방식은 안정 정렬이 아니다. 최소값을 교환하지 않고 꺼낸 뒤 그 사이의 원소를 한 칸씩 시프트하면 상대적 순서를 유지할 수 있지만, 쓰기 비용은 O(n^2)로 늘어난다.

반복 과정

입력은 길이 n의 배열 A이며, 여기서는 오름차순을 기준으로 한다. 외부 루프는 k=0..n-2를 순회하고, 내부 루프가 현재 미정렬 구간의 최소값 인덱스를 찾는다. 최소값이 이미 k 위치에 있지 않을 때만 교환하면 된다.

n≤1이면 바로 종료한다. 구현할 때는 탐색 범위 [k..n-1]를 벗어나지 않도록 하고 배열 포인터의 유효성도 확인해야 한다.

아니오아니오아니오배열 A와 길이 nn <= 1?배열 반환k = 0k < n - 1?min_idx = kt = k + 1부터 n - 1까지 최소값인덱스 탐색min_idx != k?A[k]와 A[min_idx] 스왑스왑 생략k = k + 1

C99 구현에서 확인할 부분

기본 구현은 스왑을 이용하므로 불안정하지만, 추가 공간을 사용하지 않고 최소값을 선택하는 흐름을 그대로 보여준다. 전제조건은 C99 이상, 정수 비교, 오름차순 정렬이다. 컴파일은 gcc -std=c99 -O2 sel.c -o sel로 할 수 있다.

#include <stddef.h>

void selection_sort(int a[], size_t n) {
    if (!a || n < 2) return;
    for (size_t k = 0; k + 1 < n; ++k) {
        size_t min_idx = k;
        for (size_t t = k + 1; t < n; ++t) {
            if (a[t] < a[min_idx]) min_idx = t;
        }
        if (min_idx != k) {
            int tmp = a[k];
            a[k] = a[min_idx];
            a[min_idx] = tmp;
        }
    }
}

동일한 최소값 탐색을 사용하되, 최소값을 꺼내고 [k..min_idx-1] 구간을 오른쪽으로 한 칸 옮기면 안정성을 보장할 수 있다.

#include <stddef.h>

void selection_sort_stable(int a[], size_t n) {
    if (!a || n < 2) return;
    for (size_t k = 0; k + 1 < n; ++k) {
        size_t min_idx = k;
        for (size_t t = k + 1; t < n; ++t) {
            if (a[t] < a[min_idx]) min_idx = t;
        }
        int key = a[min_idx];
        for (size_t t = min_idx; t > k; --t) a[t] = a[t - 1];
        a[k] = key;
    }
}

함수형 구현에서는 MAX 같은 상수 대신 인자 n을 루프 상한으로 사용해야 한다. 외부 루프는 k < n-1, 내부 루프는 t < n 조건을 사용하며, if(min > item[t])의 괄호를 정확히 닫아야 한다. 최소값 위치가 현재 위치와 다를 때만 스왑하는 조건도 필요하다.

void SelectionSort(int item[], int n){
    int min, min_index, k, t;
    if (!item || n < 2) return;
    for (k = 0; k < n - 1; k++) {
        min = item[k]; min_index = k;
        for (t = k + 1; t < n; t++) {
            if (min > item[t]) {
                min = item[t];  min_index = t;
            }
        }
        if (min_index != k) { // 불필요 스왑 방지
            item[min_index] = item[k];
            item[k] = min;
        }
    }
}

쓰기 비용이 제약인 상황

선택정렬은 비교 횟수가 많아 대규모 데이터에 적합하지 않다. 다만 스왑이 최대 n-1회라는 성질은 쓰기 비용이 큰 임베디드·플래시 저장장치 환경에서 의미가 있다.

소규모 데이터 처리에서도 구현의 간결성과 예측 가능한 수행 패턴을 활용할 수 있다. n ≤ 수백 수준의 데이터와 메모리 제한 환경에서는 상수 공간 사용이 장점이 된다. 알고리즘 교육에서는 루프 불변식, 선택과 교환의 관계, 다른 O(n^2) 정렬과의 성능 비교를 설명하기 좋다.

추가 메모리는 0(상수 공간)이고 스왑은 최대 n-1회이므로 메모리 사용량과 쓰기 연산을 줄일 수 있다. 구현과 검증이 단순하며 수행 패턴이 일정해 실시간성 분석에도 활용하기 쉽다.

삽입정렬·버블정렬과 비교

알고리즘 최선/평균/최악 시간 비교 횟수(근사) 스왑/쓰기 횟수 안정성 적응성(부분 정렬 시) 추가 공간
선택정렬 O(n^2)/O(n^2)/O(n^2) ~ n(n-1)/2 ≤ n-1 스왑 불안정(기본) 낮음 O(1)
삽입정렬 O(n)/O(n^2)/O(n^2) 데이터 의존 이동 다수 안정 높음 O(1)
버블정렬 O(n)/O(n^2)/O(n^2) ~ n(n-1)/2 최대 O(n^2) 스왑 안정 중간 O(1)

선택정렬의 핵심 교환 조건은 비교는 많지만 스왑은 적다는 점이다. 부분적으로 정렬된 데이터에서는 삽입정렬이 유리하고, 최적화하지 않은 버블정렬은 스왑이 과도해질 수 있다. 데이터 규모가 작거나 쓰기 비용이 큰 환경, 또는 학습과 검증 목적에서 선택적으로 사용할 수 있으며 안정성이 필요하면 시프트 기반 변형을 적용한다.

선택정렬정렬 알고리즘시간 복잡도제자리 정렬알고리즘