0Pricing
DSA Interview Prep · 강의

비교 기반이 아닌 정렬과 Python의 sort()

정수 배열에 계수 정렬과 기수 정렬을 적용하고, 내장 정렬 호출에서 Python의 Timsort가 내부적으로 작동하는 방식을 이해합니다.

비교 기반이 아닌 정렬과 Python의 sort()은(는) CoddyKit의 무료 DSA Interview Prep 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DSA Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

비교 기반 정렬의 O(n log n) 하한

원소의 순서를 오직 원소 비교를 통해 결정하는 모든 정렬 알고리즘은 최악의 경우 최소 Ω(n log n)번의 비교를 수행해야 합니다. 이는 결정 트리 논증으로 증명할 수 있습니다. n개의 원소를 정렬하려면 가능한 n!개의 순서를 구별해야 합니다. 이진 결정 트리(각 노드는 비교를 나타냄)는 최소 log₂(n!) ≈ n log₂(n)개의 수준을 필요로 합니다. 이 하한을 넘어서려면 원소에 대한 추가 정보가 필요합니다. 예를 들어 원소가 범위가 제한된 정수라는 정보가 있을 수 있습니다.

import math

for n in [5, 10, 100, 1000]:
    lower_bound = n * math.log2(n)
    factorial_log = sum(math.log2(i) for i in range(1, n+1))
    print(f'n={n}: n*log2(n)={lower_bound:.1f}, log2(n!)={factorial_log:.1f}')

# n log n is a tight bound on comparison-based sorting

계수 정렬: 빈도로 정렬

계수 정렬은 각 값의 빈도를 센 다음 그 개수로부터 정렬된 배열을 재구성합니다. 사전에 값의 범위 [0, k)를 알고 있어야 합니다. 시간 복잡도는 O(n + k), 공간 복잡도는 O(k)입니다. n에 비해 k가 작을 때(예: 0~120세의 나이 또는 한 자리 수를 정렬할 때) 계수 정렬은 모든 비교 기반 정렬보다 빠릅니다. k가 크면 O(k)의 공간 비용 때문에 실용적이지 않습니다.

def counting_sort(arr, k=None):
    if not arr: return []
    if k is None: k = max(arr) + 1
    count = [0] * k
    for n in arr:
        count[n] += 1
    result = []
    for val, freq in enumerate(count):
        result.extend([val] * freq)
    return result

arr = [4, 2, 2, 8, 3, 3, 1]
print(counting_sort(arr))  # [1, 2, 2, 3, 3, 4, 8]
# O(n + k) where k = 9 (max value + 1)

누적 개수를 활용한 안정적인 계수 정렬

안정적인 계수 정렬(키를 기준으로 객체를 정렬할 때 중요)을 수행하려면 cum[v]가 출력에서 값 v가 시작되는 위치를 나타내도록 누적 개수를 계산합니다. 입력 배열을 오른쪽에서 왼쪽으로 순회하면서 각 원소를 cum[key] - 1 위치에 배치하고 해당 위치를 1씩 줄입니다. 이렇게 하면 안정적인 정렬이 이루어져 같은 키를 가진 원소가 원래의 상대적 순서를 유지합니다.

def counting_sort_stable(arr, k):
    count = [0] * k
    for n in arr: count[n] += 1
    # Cumulative counts: count[v] = first position for value v
    for i in range(1, k): count[i] += count[i-1]
    output = [0] * len(arr)
    # Fill from right to maintain stability
    for n in reversed(arr):
        count[n] -= 1
        output[count[n]] = n
    return output

print(counting_sort_stable([4,2,2,8,3,3,1], 9))
# [1, 2, 2, 3, 3, 4, 8]

기수 정렬: 자릿수별 정렬

기수 정렬은 최하위 자릿수(LSD)부터 최상위 자릿수(MSD)까지 각 자릿수를 기준으로 정수를 정렬하며, 각 자릿수 위치에서는 안정적인 정렬(예: 계수 정렬)을 사용합니다. d번의 패스(자릿수당 한 번)가 끝나면 배열 전체가 정렬됩니다. 시간 복잡도는 O(d × (n + k))이며, d는 자릿수의 개수이고 k는 진법입니다(보통 10). W 이하로 제한된 n개의 정수에 대해 d = log_k(W)이므로 전체 시간 복잡도는 O(n log_k(W))입니다.

def radix_sort(arr):
    if not arr: return []
    max_val = max(arr)
    exp = 1  # current digit position (1, 10, 100, ...)
    while max_val // exp > 0:
        arr = counting_sort_by_digit(arr, exp)
        exp *= 10
    return arr

def counting_sort_by_digit(arr, exp):
    n = len(arr)
    output = [0] * n
    count = [0] * 10
    for n_ in arr: count[(n_ // exp) % 10] += 1
    for i in range(1, 10): count[i] += count[i-1]
    for n_ in reversed(arr):
        d = (n_ // exp) % 10
        count[d] -= 1
        output[count[d]] = n_
    return output

print(radix_sort([170, 45, 75, 90, 802, 24, 2, 66]))
# [2, 24, 45, 66, 75, 90, 170, 802]

버킷 정렬: 버킷으로 분배하기

버킷 정렬은 값의 범위에 따라 원소를 고정된 개수의 버킷에 분배하고, 각 버킷을 정렬한 다음(작은 버킷에는 삽입 정렬 사용) 버킷을 이어 붙입니다. [0, 1) 범위에 균등하게 분포된 데이터에서는 n개의 버킷을 사용할 때 평균 시간 복잡도가 O(n)입니다. 시간 복잡도는 평균 O(n + k), 최악의 경우 O(n²)입니다(모든 원소가 하나의 버킷에 들어가는 경우). 데이터 분포를 알고 있고 대략 균등할 때 가장 유용합니다.

def bucket_sort(arr):
    if not arr: return []
    n = len(arr)
    min_v, max_v = min(arr), max(arr)
    if min_v == max_v: return arr[:]
    buckets = [[] for _ in range(n)]
    # Map each value to a bucket index
    for v in arr:
        idx = int((v - min_v) / (max_v - min_v + 1e-9) * n)
        idx = min(idx, n - 1)
        buckets[idx].append(v)
    result = []
    for bucket in buckets:
        bucket.sort()  # insertion sort for small buckets
        result.extend(bucket)
    return result

print(bucket_sort([0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21]))
# sorted list

파이썬의 팀소트 내부 동작

파이썬의 sorted()와 list.sort()는 팀 피터스가 2002년에 설계한 팀소트를 사용합니다. 팀소트는 병합 정렬과 삽입 정렬을 결합한 방식입니다. 팀소트는 이미 정렬된 부분 수열인 ‘자연 런’을 찾고, 삽입 정렬을 사용해 최대 64개 원소의 런을 만듭니다. 그런 다음 병합 정렬을 사용해 런을 병합하며, 몇 가지 최적화를 적용합니다. 여기에는 한 런이 우세할 때 여러 원소를 한꺼번에 건너뛰는 갤로핑과 런 길이 쌓기가 포함됩니다.

# Timsort properties:
# - Stable
# - O(n log n) worst case
# - O(n) best case (data already sorted)
# - O(n) auxiliary space
# - Highly optimised for real-world data with runs

import time

# Nearly sorted data: Timsort is extremely fast
nearly_sorted = list(range(10000))
nearly_sorted[-1] = 0  # one mis-placed element

t = time.perf_counter()
not_used = sorted(nearly_sorted)
elapsed = time.perf_counter() - t
print(f'Timsort on nearly-sorted n=10000: {elapsed*1000:.3f} ms')

파이썬의 sort()와 sorted()의 주요 차이

list.sort()는 제자리에서 정렬하고 None을 반환하며, 리스트에서만 사용할 수 있습니다. sorted(iterable)은 튜플, 생성기, 딕셔너리 등 모든 반복 가능한 객체에서 사용할 수 있고 새로운 리스트를 반환합니다. 두 함수 모두 key와 reverse 매개변수를 받습니다. 흔히 발생하는 오류는 lst.sort()의 반환값을 변수에 할당한 뒤 왜 None인지 의아해하는 것입니다. 정렬된 결과가 필요하면서 원본을 유지하고 싶다면 항상 sorted()을 사용하십시오.

nums = [3, 1, 4, 1, 5, 9]

# in-place: returns None
result = nums.sort()
print(result)  # None  (common bug!)
print(nums)    # [1, 1, 3, 4, 5, 9]  (modified)

nums2 = [3, 1, 4, 1, 5, 9]
# out-of-place: returns new list
result2 = sorted(nums2)
print(result2)  # [1, 1, 3, 4, 5, 9]
print(nums2)    # [3, 1, 4, 1, 5, 9]  (unchanged)

면접에서 사용하는 사용자 지정 정렬 키

파이썬의 sort는 각 원소마다 한 번씩 평가되는 key 함수를 받습니다(C 언어의 비교 함수가 모든 쌍에 대해 호출되는 것과 다릅니다). 면접에서 자주 사용하는 정렬 키로는 문자열 길이를 위한 len, 내림차순을 위한 lambda x: -x, 다중 키 정렬을 위한 lambda x: (x[1], x[0]), 대소문자를 구분하지 않는 정렬을 위한 str.lower가 있습니다. 파이썬의 sort는 안정성이 보장되므로 다중 키 정렬이 올바르게 작동합니다.

# Sort by length, then alphabetically
words = ['banana', 'fig', 'apple', 'date', 'kiwi']
print(sorted(words, key=lambda w: (len(w), w)))
# ['fig', 'date', 'kiwi', 'apple', 'banana']

# Sort integers as strings (largest concatenation first)
nums = [3, 30, 34, 5, 9]
print(sorted(map(str, nums), key=lambda a: a*10, reverse=True))
# ['9', '5', '34', '3', '30']  => '9534330'

# Descending sort
print(sorted([3,1,4,1,5], reverse=True))  # [5,4,3,1,1]

면접에서 각 정렬을 사용하는 경우

상황에 맞는 정렬을 선택하십시오:

  • 파이썬의 sorted()/list.sort() 사용: 모든 면접 문제의 기본 선택 — 팀소트가 최적입니다
  • 계수 정렬: 값이 작은 범위의 정수로 제한되어 있을 때(0부터 k까지이며 k가 작을 때)
  • 기수 정렬: 비트 너비나 자릿수 개수가 알려진 정수를 많이 정렬할 때
  • 버킷 정렬: 알려진 범위에서 실수가 균등하게 분포할 때
  • 병합 정렬 직접 구현: 처음부터 안정적인 O(n log n) 정렬을 작성하라는 요청을 받았을 때

# Problem: sort array of 0s, 1s, 2s efficiently
# Counting sort: O(n), O(1) space  (k=3 is tiny)

def sort_012(arr):
    count = [0, 0, 0]
    for n in arr:
        count[n] += 1
    i = 0
    for val in range(3):
        for _ in range(count[val]):
            arr[i] = val; i += 1

arr = [2, 0, 2, 1, 1, 0]
sort_012(arr)
print(arr)  # [0, 0, 1, 1, 2, 2]

정렬 없이 정렬하기: 힙으로 상위 k개 찾기

많은 면접 문제는 전체 정렬을 요구하지 않고 ‘정렬과 유사한’ 결과를 요구합니다. 상위 k개 원소 찾기에는 크기가 k인 최소 힙을 사용하면 O(n log k)에 해결할 수 있어, k가 n보다 훨씬 작을 때 O(n log n)보다 빠릅니다. k번째로 큰 원소 찾기는 퀵셀렉트를 사용하면 평균 O(n)입니다. 중앙값 찾기는 원소를 삽입할 때마다 두 힙을 사용하는 방식으로 O(log n)에 처리할 수 있습니다. 이러한 부분 정렬 방식은 전체 정렬보다 빠른 대안으로 알아 둘 가치가 있습니다.

import heapq

# Top-k with heap: O(n log k)
def top_k(nums, k):
    return heapq.nlargest(k, nums)  # uses heap of size k internally

print(top_k([3,2,1,5,6,4], 2))    # [6, 5]

# kth largest: quickselect O(n) average
import random
def kth_largest(nums, k):
    def _select(lo, hi, target):
        if lo >= hi: return nums[lo]
        rand_i = random.randint(lo, hi)
        nums[rand_i], nums[hi] = nums[hi], nums[rand_i]
        pivot = nums[hi]; i = lo - 1
        for j in range(lo, hi):
            if nums[j] >= pivot: i+=1; nums[i],nums[j]=nums[j],nums[i]
        nums[i+1],nums[hi]=nums[hi],nums[i+1]
        p = i + 1
        if p == target: return nums[p]
        return _select(lo, p-1, target) if target < p else _select(p+1, hi, target)
    return _select(0, len(nums)-1, k-1)

print(kth_largest([3,2,1,5,6,4], 2))  # 5

다중 키 정렬에서의 정렬 안정성

안정성은 다중 키 정렬을 올바르게 수행할 수 있게 합니다. 먼저 보조 키를 기준으로 안정적으로 정렬한 다음, 주 키를 기준으로 안정적으로 정렬하십시오. 그러면 주 키가 같은 원소들 사이에서 보조 키의 순서가 유지됩니다. 이 기법은 데이터베이스의 (ORDER BY col1, col2)와 기수 정렬에서 사용됩니다(전체 알고리즘이 올바르게 작동하려면 각 자릿수 패스가 안정적이어야 합니다). 파이썬의 sort는 항상 안정적이므로 이 패턴이 안정적으로 작동합니다.

data = [
    ('Alice', 'Math',    90),
    ('Bob',   'Science', 85),
    ('Carol', 'Math',    90),
    ('Dave',  'Science', 90),
]
# Sort by score DESC, then by subject ASC (for ties)
# Step 1: sort by subject (secondary)
data.sort(key=lambda x: x[1])
# Step 2: sort by score DESC (primary, stable)
data.sort(key=lambda x: x[2], reverse=True)
for row in data:
    print(row)
# All score=90 rows: Math before Science (preserved from step 1)

빠른 확인

이번 수업에서 다룬 자료 구조 및 알고리즘 — 코딩 인터뷰 준비 개념에 대한 이해도를 확인해 보십시오.

수업 복습

이번 수업에서 배운 내용은 다음과 같습니다. 비교 기반 정렬은 O(n log n)보다 낮아질 수 없으며, 이 하한을 깨려면 범위가 제한된 정수처럼 비교 이외의 정보가 필요합니다. 또한 계수 정렬은 빈도를 세어 O(n + k)을 달성하고, 기수 정렬은 O(d × (n + k))의 전체 복잡도로 자릿수를 처리하며, 버킷 정렬은 균등한 분포를 활용해 평균 O(n)을 달성합니다. 그리고 파이썬의 팀소트는 실전의 기본 선택으로, 안정적이고 최악의 경우 O(n log n), 최선의 경우 O(n)이며 실제 데이터에서는 직접 작성한 어떤 대안보다도 빠릅니다. 다음으로 고전적인 이진 탐색을 완전히 익혀 보겠습니다.

자주 묻는 질문

“비교 기반이 아닌 정렬과 Python의 sort()” 강의는 무료인가요?

네 — “비교 기반이 아닌 정렬과 Python의 sort()” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DSA Interview Prep 강의 전체를 잠금 해제할 수 있습니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

“비교 기반이 아닌 정렬과 Python의 sort()”에서 뭘 배우나요?

정수 배열에 계수 정렬과 기수 정렬을 적용하고, 내장 정렬 호출에서 Python의 Timsort가 내부적으로 작동하는 방식을 이해합니다. 브라우저에서 직접 실행하는 실습 코드로 DSA Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

DSA Interview Prep을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 DSA Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“비교 기반이 아닌 정렬과 Python의 sort()” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 DSA Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 DSA Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 버블 정렬과 삽입 정렬
  2. 병합 정렬: 분할, 정렬, 병합
  3. 퀵 정렬과 피벗 선택
  4. 비교 기반이 아닌 정렬과 Python의 sort()
← DSA Interview Prep(으)로 돌아가기