Selection Sort(선택 정렬)는 현재 구간에서 최솟값(또는 최댓값)을 “선택”한 뒤, 그 값을 구간의 시작 위치로 한 번 교환하여 정렬을 진행하는 방식입니다.
Bubble Sort가 “인접한 두 값을 비교하며 필요할 때마다 교환(인접 교환)”을 반복한다면, Selection Sort는 “현재 구간 전체를 훑어 최소(또는 최대)를 찾고, 그 결과를 한 번 교환(선택 후 교환)”합니다. 두 알고리즘 모두 비교 기반 정렬이며, 비교 흐름과 교환(스왑) 흐름이 어떻게 구성되는지를 기준으로 보면 구조가 선명해집니다.
정렬 비용을 “빠르다/느리다”로 단순화하면, 실제로 비용을 만드는 요소(비교 횟수, 데이터 이동/교환 횟수, 분기 패턴)를 놓치기 쉽습니다. Selection Sort는 특히 비교 횟수는 많지만 교환 횟수는 제한되는 구조이므로, 비교/교환 관점으로 보는 것이 가장 정확합니다.
1. Selection Sort의 구조와 동작 방식
1.1 기본 아이디어
정렬되지 않은 구간을 [i .. N-1]라고 할 때, Selection Sort는 매 Pass마다 다음을 수행합니다.
- i 위치에 들어갈 값(최솟값)을 구간 전체에서 찾는다.
- 찾은 최솟값의 인덱스(minIndex)를 i와 교환한다.
- i를 1 증가시키며 정렬된 구간을 확장한다.
1.2 Pass 단위 동작
Pass i는 “i에 들어갈 최솟값을 확정”하는 단계입니다.
- 시작 시점:
i부터 끝까지는 정렬되지 않은 구간 - 진행:
j = i+1 .. N-1을 순회하며 최솟값 후보를 갱신 - 종료: 최솟값 인덱스가 i가 아니라면 딱 한 번 스왑
한 Pass가 끝나면 i 위치의 값은 최종 위치가 확정됩니다.
즉, “정렬된 prefix가 1칸씩 늘어난다”는 점에서 구조가 명확합니다.
※ 동작을 시각적으로 확인해보기
아래 링크는 Selection Sort의 현재 구간 전체를 탐색하여 최솟값(또는 최댓값)을 선택한 뒤, 한 번의 교환으로 위치를 확정하는 과정을 단계별로 시각화해 주는 도구입니다. 각 패스(pass)마다 “선택 → 1회 교환”이 어떻게 이루어지는지 확인할 수 있습니다.
https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html
2. 왜 O(N²)이 되는가 (비교 흐름 기준)
Selection Sort의 핵심 비용은 “최솟값 탐색”입니다. Pass i에서 최솟값을 찾기 위해 비교하는 횟수는 정렬되지 않은 구간 길이에 비례합니다.
- Pass 0: (N-1)번 비교 (0 뒤의 모든 값과 비교)
- Pass 1: (N-2)번 비교
- ...
- Pass N-2: 1번 비교
따라서 총 비교 횟수는 다음 합으로 누적됩니다.
(N - 1) + (N - 2) + ... + 2 + 1
= N(N - 1) / 2
이 누적 비교 구조 때문에 시간 복잡도는 O(N²)로 표현됩니다.
Bubble Sort도 평균/최악 관점에서 비교가 누적되어 O(N²)로 분류되는 경우가 많습니다. 다만 두 알고리즘의 차이는 교환(스왑) 횟수에서 더 선명합니다.
- Bubble Sort: 인접 교환이 반복되므로 교환이 매우 자주 발생할 수 있음
- Selection Sort: Pass당 최대 1회 스왑 → 전체 스왑 횟수는 최대 (N-1)
즉, 비교 횟수는 유사한 범위에 있으나, 교환 횟수(데이터 이동) 구조는 서로 다르다는 점이 핵심입니다.
3. Selection Sort의 특징과 한계
3.1 교환 횟수가 최대 N-1로 제한됨
Selection Sort는 매 Pass마다 “최솟값을 찾고 마지막에 한 번만 교환”합니다. 따라서 스왑은 최대 (N-1)번입니다.
데이터 이동 비용이 큰 타입(큰 구조체, 비용이 큰 복사/이동)에서는 “비교는 많이 하지만 이동은 제한한다”는 특성이 의미를 가질 수 있습니다. (단, C#의 일반적인 int 배열에서는 이동 비용이 매우 작습니다.)
3.2 이미 정렬된 경우에도 비교를 줄일 수 없음
Selection Sort는 “최솟값 탐색”을 Pass마다 끝까지 수행합니다. 입력이 이미 정렬되어 있어도, 각 Pass는 구간 전체를 확인해야 “최솟값이 i에 있다”는 사실을 알 수 있습니다.
즉, 비교 횟수가 입력 상태에 크게 의존하지 않는 구조입니다. 이 점은 “정렬 여부를 빠르게 감지해 루프를 단축”할 여지가 있는 방식과 대비됩니다.
3.3 안정 정렬(Stable Sort)이 아님
Selection Sort는 각 Pass에서 최솟값(또는 최댓값)을 찾은 뒤, 현재 위치 i와 직접 교환(swap)합니다. 이 교환은 “최솟값을 앞에 배치한다”는 목적만 달성하며, 중간에 있던 원소들의 상대 순서는 고려하지 않습니다. 따라서 동일한 키를 가진 원소가 존재할 때, 정렬 결과에서 그 원소들의 상대 순서가 유지된다고 보장할 수 없습니다. 즉, 일반적인 Selection Sort는 안정 정렬(Stable Sort)이 아닙니다.
예를 들어 아래 배열에서, 숫자(키)는 같지만 꼬리표(A/B)가 다른 두 원소가 있다고 가정합니다. (A/B는 “원래의 상대 순서”를 구분하기 위한 표기입니다.)
[(2,A), (2,B), (1,C)]
Pass 0에서 구간 [0..2]의 최솟값은 (1,C)이며, 인덱스 2에 있습니다. Selection Sort는 이 최솟값을 현재 위치 i = 0과 교환합니다.
swap(0, 2)
[(1,C), (2,B), (2,A)]
이 결과에서 확인할 수 있듯이, 정렬 전에는 (2,A)가 (2,B)보다 앞에 있었지만, 정렬 과정의 교환으로 인해 최종 배열에서는 (2,B)가 (2,A)보다 앞에 위치하게 됩니다. 즉, 동일 키(2)를 가진 원소들의 상대 순서가 바뀔 수 있으므로 안정 정렬이 아닙니다.
4. 실행 흐름을 예제로 확인하기
예제 배열: [64, 25, 12, 22, 11]
정렬 기준: 오름차순
Pass 0 (i = 0)
현재 기준 위치 i=0에 들어갈 최솟값을 구간 [0..4]에서 찾습니다.
- 초기: minIndex = 0 (값 64)
- j=1: 25 < 64 → minIndex = 1
- j=2: 12 < 25 → minIndex = 2
- j=3: 22 < 12 → 변화 없음
- j=4: 11 < 12 → minIndex = 4
Pass 0 종료 시점에 최솟값(11)의 위치는 4이고, i(0)와 다르므로 스왑을 수행합니다.
스왑(0,4) → [11, 25, 12, 22, 64]
0번 인덱스의 값(11)이 최종 위치로 확정됩니다.
Pass 1 (i = 1)
구간 [1..4]에서 최솟값을 찾아 1에 배치합니다.
- 초기: minIndex = 1 (값 25)
- j=2: 12 < 25 → minIndex = 2
- j=3: 22 < 12 → 변화 없음
- j=4: 64 < 12 → 변화 없음
스왑(1,2) → [11, 12, 25, 22, 64]
1번 인덱스의 값(12)이 최종 위치로 확정됩니다.
Pass 2 (i = 2)
구간 [2..4]에서 최솟값을 찾아 2에 배치합니다.
- 초기: minIndex = 2 (값 25)
- j=3: 22 < 25 → minIndex = 3
- j=4: 64 < 22 → 변화 없음
스왑(2,3) → [11, 12, 22, 25, 64]
2번 인덱스의 값(22)이 최종 위치로 확정됩니다.
Pass 3 (i = 3)
구간 [3..4]에서 최솟값을 찾습니다.
- 초기: minIndex = 3 (값 25)
- j=4: 64 < 25 → 변화 없음
minIndex가 i와 같으므로 스왑이 발생하지 않습니다.
배열은 [11, 12, 22, 25, 64] 상태로 유지되며, 정렬이 완료됩니다.
이 예제에서 확인할 수 있는 구조는 다음과 같습니다.
- 각 Pass는 “최솟값 탐색(비교 누적) + 필요 시 단 1회 스왑”으로 구성
- Pass 종료 시점에 i 위치가 확정되며, 정렬된 구간이 한 칸씩 증가
5. C# 기반 Selection Sort 직접 구현 코드
아래 코드는 int[]에 대한 기본 Selection Sort 구현입니다.
가독성을 우선하며, 불필요한 트릭(비트 스왑, LINQ, 복잡한 최적화)은 포함하지 않습니다.
using System;
namespace Daebak.Common.Algorithms.Sorting
{
public static class SelectionSort
{
public static void Sort(int[] array)
{
if (array == null)
{
throw new ArgumentNullException(nameof(array));
}
int n = array.Length;
if (n <= 1)
{
return;
}
for (int i = 0; i < n - 1; i++)
{
int minIndex = i;
for (int j = i + 1; j < n; j++)
{
if (array[j] < array[minIndex])
{
minIndex = j;
}
}
if (minIndex != i)
{
Swap(array, i, minIndex);
}
}
}
private static void Swap(int[] array, int a, int b)
{
int temp = array[a];
array[a] = array[b];
array[b] = temp;
}
}
}
구조적으로 중요한 포인트는 다음 두 가지입니다.
- 내부 루프(
j)는 “최솟값 탐색”만 수행하며, 탐색 중에는 교환하지 않습니다. - 스왑은 Pass 종료 시점에 조건적으로 1회 수행됩니다(
minIndex != i).
6. Unity 환경에서 테스트 해보기
using UnityEngine;
using Daebak.Common.Algorithms.Sorting;
public class Test_SelectionSort : MonoBehaviour
{
private void Start()
{
int[] data = new int[] { 64, 25, 12, 22, 11 };
Debug.Log("Before Sort : " + string.Join(", ", data));
SelectionSort.Sort(data);
Debug.Log("After Sort : " + string.Join(", ", data));
}
}
이 테스트는 다음을 확인하기에 충분합니다.
- 정렬 전/후 배열 상태가 명확히 출력되는지
- 정렬 로직이 Unity 환경과 분리되어 재사용 가능한지
출력 결과
Before Sort : 64, 25, 12, 22, 11
After Sort : 11, 12, 22, 25, 64
7. 마무리
Selection Sort는 “구간에서 최솟값을 선택하고, Pass당 최대 1회 교환”이라는 구조가 명확한 정렬입니다. 따라서 비교/교환 관점에서 비용을 분해해 이해하기에 좋은 기준점이 됩니다.
- 비교: (N-1) + (N-2) + ... + 1 형태로 누적되어 O(N²)
- 교환: Pass당 최대 1회 → 전체 최대 (N-1)회
Bubble Sort가 “인접 비교/인접 교환”을 반복해 정렬이 진행되는 구조라면, Selection Sort는 “선택(탐색) 후 교환”으로 정렬된 구간을 확장하는 구조입니다. 이 차이를 비교/교환 관점으로 정리해두면, Insertion Sort처럼 “이동이 많은 대신 비교 흐름이 달라지는 방식”이나, Quick Sort처럼 “분할을 통해 비교 구조 자체가 달라지는 방식”을 해석할 때도 기준이 흔들리지 않습니다.
※ 다음 글에서는 Insertion Sort를 살펴보며, 이미 정렬된 구간에 새로운 요소를 삽입하는 방식이 데이터 이동(shift)과 입력 상태에 따른 성능 차이를 어떻게 만들어내는지를 중심으로 정리합니다.
'자료 구조' 카테고리의 다른 글
| Unity 개발자를 위한 C# Shell Sort(셸 정렬) 구현 (0) | 2026.01.15 |
|---|---|
| Unity 개발자를 위한 C# Insertion Sort(삽입 정렬) 구현 (0) | 2026.01.14 |
| Unity 개발자를 위한 C# Bubble Sort(버블 정렬) 구현 (0) | 2026.01.14 |
| Unity 개발자를 위한 정렬(Sort) 개념과 시간 복잡도 이해 (0) | 2026.01.13 |
| Unity 개발자를 위한 시간 복잡도(Big O) 개념 정리 (0) | 2026.01.13 |