모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

8강 / 전체 9강

정렬 기초

9분 읽기 조회 0

버블·선택·삽입 정렬의 O(n²) 원리와 안정 정렬의 의미, key 인자로 다중 기준 정렬

🎯 학습 목표

이 강을 마치면 버블·선택·삽입 정렬의 동작을 설명하고 왜 셋 다 O(n²)인지 근거를 댈 수 있습니다. 그리고 정렬의 안정성(stability)이 무엇이며 언제 문제가 되는지, 파이썬 sort()와 sorted()를 key 인자로 자유롭게 쓰는 법을 익히게 됩니다.

실무와 코딩 테스트에서 정렬을 직접 구현할 일은 거의 없습니다. 그래도 배우는 이유는 "O(n²) 알고리즘이 어떻게 생겼는지"를 보는 가장 좋은 교재이기 때문입니다. 그리고 안정성은 실제로 문제 정답을 가릅니다.

💡 O(n²) 정렬 세 가지

세 알고리즘은 접근 방식이 다르지만 모두 n개를 n번 다루므로 O(n²)입니다.

세 가지 정렬의 아이디어

  ① 버블 정렬 — 옆 사람과 비교해서 큰 쪽을 뒤로
     [5, 2, 4, 1]
      ↕            5>2 → 교환   [2, 5, 4, 1]
         ↕         5>4 → 교환   [2, 4, 5, 1]
            ↕      5>1 → 교환   [2, 4, 1, 5]  ← 5가 제자리
     한 바퀴 돌 때마다 가장 큰 값이 뒤로 밀려 간다

  ② 선택 정렬 — 남은 것 중 가장 작은 것을 골라 앞에 놓기
     [5, 2, 4, 1]  최솟값 1 → 맨 앞과 교환   [1, 2, 4, 5]
     [_, 2, 4, 5]  최솟값 2 → 이미 제자리     [1, 2, 4, 5]
     [_, _, 4, 5]  최솟값 4 → 이미 제자리     [1, 2, 4, 5]

  ③ 삽입 정렬 — 카드를 손에 정리하듯 알맞은 자리에 끼우기
     [5 | 2, 4, 1]   2를 5 앞에 끼움   [2, 5 | 4, 1]
     [2, 5 | 4, 1]   4를 5 앞에 끼움   [2, 4, 5 | 1]
     [2, 4, 5 | 1]   1을 맨 앞에 끼움  [1, 2, 4, 5]

  ▲ 왜 전부 O(n²)인가
    "n개 각각에 대해, 나머지 n개를 훑는다"
    → n × n

  ▲ 그래도 차이는 있다
    · 삽입 정렬은 이미 정렬된 입력에서 O(n) — 거의 안 움직인다
    · 선택 정렬은 어떤 입력이든 항상 O(n²) — 교환 횟수는 가장 적다
    · 버블 정렬은 교환이 가장 많다 — 실무에서 거의 안 쓴다
정렬최선평균·최악안정성추가 메모리
버블O(n)O(n²)안정O(1)
선택O(n²)O(n²)불안정O(1)
삽입O(n)O(n²)안정O(1)

삽입 정렬의 최선 O(n)이 의외로 중요합니다. 거의 정렬된 데이터에서는 매우 빠르기 때문에, 실제 정렬 라이브러리들이 작은 구간을 처리할 때 삽입 정렬을 섞어 씁니다. 9강에서 다시 나옵니다.

📖 안정 정렬이 왜 중요한가

안정 정렬(stable sort)은 값이 같은 원소들의 원래 순서를 그대로 유지하는 정렬입니다. 말로는 사소해 보이지만, 두 가지 기준으로 정렬할 때 결정적입니다.

안정성이 결과를 바꾸는 예

  학생 데이터 (입력 순서대로)
   ("김", 90), ("이", 85), ("박", 90), ("최", 85)

  점수 내림차순으로 정렬하면?

  [안정 정렬]  같은 점수는 원래 순서 유지
   ("김", 90), ("박", 90), ("이", 85), ("최", 85)
       ↑ 김이 박보다 먼저 (입력 순서 그대로)

  [불안정 정렬]  같은 점수의 순서는 보장 안 됨
   ("박", 90), ("김", 90), ("최", 85), ("이", 85)
       ↑ 순서가 뒤집힐 수 있다

  ▲ 이것이 왜 유용한가 — 다중 기준 정렬

   "점수 내림차순, 점수가 같으면 이름 오름차순"

   안정 정렬이면 두 번 정렬하면 된다
    1) 이름 오름차순으로 정렬
    2) 점수 내림차순으로 정렬
       → 점수가 같은 것들은 1번의 이름 순서가 그대로 남는다

   ※ 순서가 중요하다. '덜 중요한 기준'부터 정렬한다.

  ▲ 파이썬의 sort()·sorted() 는 안정 정렬이다.
    공식 문서에 명시돼 있으므로 믿고 활용할 수 있다.

물론 파이썬에서는 key 하나로 다중 기준을 한 번에 지정하는 방법도 있습니다. 다음 예제에서 두 방식을 모두 보겠습니다.

💻 실전 예제 — 직접 구현과 파이썬 정렬

def insertion_sort(arr):
    """삽입 정렬 — 원본을 직접 정렬한다"""
    for i in range(1, len(arr)):
        key = arr[i]                    # 끼워 넣을 값
        j = i - 1
        while j >= 0 and arr[j] > key:  # 나보다 큰 값들을
            arr[j + 1] = arr[j]         # 한 칸씩 뒤로 민다
            j -= 1
        arr[j + 1] = key                # 빈 자리에 넣는다
    return arr


def selection_sort(arr):
    """선택 정렬 — 남은 구간의 최솟값을 앞으로"""
    n = len(arr)
    for i in range(n):
        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


students = [("김", 90), ("이", 85), ("박", 90), ("최", 85)]

# 방법 1 : key 하나로 다중 기준 (점수 내림차순, 이름 오름차순)
by_key = sorted(students, key=lambda s: (-s[1], s[0]))

# 방법 2 : 안정 정렬을 이용해 두 번 나눠서
tmp = sorted(students, key=lambda s: s[0])          # 덜 중요한 기준 먼저
by_two_pass = sorted(tmp, key=lambda s: s[1], reverse=True)

print(insertion_sort([5, 2, 4, 1]))   # [1, 2, 4, 5]
print(selection_sort([5, 2, 4, 1]))   # [1, 2, 4, 5]
print(by_key)                          # 두 결과가 같다
print(by_key == by_two_pass)           # True

코드를 줄 단위로 보겠습니다.

  • for i in range(1, len(arr)) — 삽입 정렬은 0번은 이미 정렬된 것으로 보고 1번부터 시작합니다.
  • while j >= 0 and arr[j] > key — 왼쪽으로 가면서 key보다 큰 값을 계속 뒤로 밉니다. j >= 0을 먼저 확인해야 인덱스 오류가 안 납니다.
  • arr[j] > key에서 등호를 넣지 않는 것이 안정성의 비결입니다. 같은 값을 만나면 멈추므로 원래 순서가 보존됩니다. >=로 바꾸면 불안정해집니다.
  • arr[i], arr[min_idx] = arr[min_idx], arr[i] — 파이썬의 동시 할당으로 임시 변수 없이 교환합니다.
  • 선택 정렬의 이 교환이 멀리 떨어진 원소를 건너뛰며 옮기기 때문에 같은 값의 순서가 뒤바뀔 수 있습니다. 그래서 불안정합니다.
  • key=lambda s: (-s[1], s[0]) — 튜플을 반환하면 앞 요소부터 차례로 비교합니다. 점수에 음수를 붙여 내림차순을 만들고, 같으면 이름 오름차순으로 갑니다. 숫자에만 쓸 수 있는 기법입니다.
  • sorted(tmp, key=..., reverse=True) — 두 번 정렬하는 방식입니다. 안정 정렬이기 때문에 첫 정렬 결과가 유지됩니다. 문자열을 내림차순으로 해야 할 때처럼 음수를 쓸 수 없는 경우에 유용합니다.

sort()와 sorted()의 차이도 정리해 두겠습니다. lst.sort()는 원본을 바꾸고 None을 반환합니다. sorted(lst)는 새 리스트를 만들어 반환하고 원본은 그대로입니다. a = lst.sort()라고 쓰면 a가 None이 되는 실수가 매우 흔합니다.

⚠️ 자주 하는 실수

첫째, sort()의 반환값을 씁니다. sorted_list = my_list.sort()는 None을 담습니다. 원본을 바꾸려면 lst.sort(), 새 리스트가 필요하면 sorted(lst)입니다.

둘째, key와 cmp를 헷갈립니다. 파이썬 3에는 두 값을 비교하는 cmp 인자가 없습니다. key는 "각 원소를 무엇으로 볼 것인가"를 지정하는 함수입니다. 꼭 비교 함수가 필요하면 functools.cmp_to_key로 감싸야 합니다.

셋째, 정렬을 반복문 안에서 합니다. sorted()는 O(n log n)입니다. 반복문 안에 넣으면 전체가 O(n² log n)이 됩니다. 한 번만 정렬해 두고 재사용하는 구조로 바꾸세요.

넷째, 안정성을 확인하지 않고 두 번 정렬합니다. 다중 기준을 두 번의 정렬로 처리하려면 덜 중요한 기준을 먼저 정렬해야 합니다. 순서를 반대로 하면 결과가 완전히 달라집니다.

다섯째, 자료형이 섞인 리스트를 정렬합니다. [1, "a"]처럼 비교할 수 없는 자료형이 섞이면 오류가 납니다. 입력을 받을 때 정수로 변환했는지 확인하세요. 문자열 "10"과 "9"를 정렬하면 "10"이 앞에 옵니다.

📝 핵심 요약 + 다음 강 예고

이번 강의 내용을 정리합니다.

#기억할 것
1버블·선택·삽입은 전부 O(n²)
2삽입 정렬은 거의 정렬된 입력에서 O(n)
3안정 정렬은 같은 값의 원래 순서를 지킨다
4파이썬 정렬은 안정하다
5다중 기준은 key에 튜플을 돌려주면 된다
6두 번 정렬할 땐 덜 중요한 기준부터
7sort()는 원본 변경, sorted()는 새 리스트

지금 해 볼 것은 students를 이름 오름차순으로 먼저 정렬하지 않고 점수만 정렬해 보는 것입니다. 안정 정렬 덕분에 입력 순서가 그대로 남는 것을 직접 확인할 수 있습니다.

다음 9강 「효율적 정렬」에서는 O(n log n) 정렬로 넘어갑니다. 병합 정렬과 퀵 정렬이 어떻게 log n을 만들어 내는지, 그리고 파이썬 sort()의 내부 알고리즘이 무엇인지 다루겠습니다.

📚 참고 자료

관련 주제

  • 정렬
  • 버블 정렬
  • 선택 정렬
  • 삽입 정렬
  • 안정 정렬
  • sorted
  • key 인자
  • 다중 기준 정렬
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 코딩 테스트 합격 알고리즘 30강
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

코딩 테스트 합격 알고리즘 30강

이 강의가 속한 시리즈는 총 9강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.

댓글

0/1000

불러오는 중...