개념

데이터를 특정 순서대로 나열하는 알고리즘

오름차순, 내림차순 등 원하는 기준으로 데이터를 정렬


정렬 알고리즘 비교

알고리즘최선평균최악공간복잡도안정성
버블 정렬O(n)O(n²)O(n²)O(1)안정
선택 정렬O(n²)O(n²)O(n²)O(1)불안정
삽입 정렬O(n)O(n²)O(n²)O(1)안정
병합 정렬O(n log n)O(n log n)O(n log n)O(n)안정
퀵 정렬O(n log n)O(n log n)O(n²)O(log n)불안정
힙 정렬O(n log n)O(n log n)O(n log n)O(1)불안정

Python 내장 정렬

1. sort() vs sorted()

# sort() - 원본 리스트를 정렬 (반환값 None)
arr = [3, 1, 4, 1, 5]
arr.sort()
print(arr)  # [1, 1, 3, 4, 5]
 
# sorted() - 새로운 정렬된 리스트 반환
arr = [3, 1, 4, 1, 5]
sorted_arr = sorted(arr)
print(sorted_arr)  # [1, 1, 3, 4, 5]
print(arr)         # [3, 1, 4, 1, 5] (원본 유지)
 
# 시간복잡도: O(n log n) - Timsort 사용

2. 다양한 정렬 옵션

# 내림차순
arr = [3, 1, 4, 1, 5]
arr.sort(reverse=True)
print(arr)  # [5, 4, 3, 1, 1]
 
# key 함수로 정렬 기준 지정
words = ['banana', 'pie', 'Washington', 'book']
 
# 길이순 정렬
words.sort(key=len)
print(words)  # ['pie', 'book', 'banana', 'Washington']
 
# 대소문자 무시 정렬
words.sort(key=str.lower)
print(words)  # ['banana', 'book', 'pie', 'Washington']
 
# 튜플 리스트 정렬
students = [('Alice', 85), ('Bob', 75), ('Charlie', 90)]
 
# 점수로 정렬
students.sort(key=lambda x: x[1])
print(students)  # [('Bob', 75), ('Alice', 85), ('Charlie', 90)]
 
# 여러 기준으로 정렬 (나이 먼저, 같으면 이름)
people = [('Alice', 30), ('Bob', 25), ('Charlie', 30)]
people.sort(key=lambda x: (x[1], x[0]))
print(people)  # [('Bob', 25), ('Alice', 30), ('Charlie', 30)]

기본 정렬 알고리즘

1. 버블 정렬 (Bubble Sort)

인접한 두 요소를 비교하여 교환하는 방식

def bubble_sort(arr):
    n = len(arr)
 
    for i in range(n):
        swapped = False
 
        # 마지막 i개는 이미 정렬됨
        for j in range(0, n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                swapped = True
 
        # 교환이 없으면 이미 정렬됨
        if not swapped:
            break
 
    return arr
 
# 테스트
print(bubble_sort([64, 34, 25, 12, 22, 11, 90]))
# [11, 12, 22, 25, 34, 64, 90]
 
# 시간복잡도: O(n²)
# 공간복잡도: O(1)
# 안정 정렬

특징: 간단하지만 느림, 거의 정렬된 데이터에 유리


2. 선택 정렬 (Selection Sort)

최솟값을 찾아 맨 앞과 교환하는 방식

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
 
# 테스트
print(selection_sort([64, 25, 12, 22, 11]))
# [11, 12, 22, 25, 64]
 
# 시간복잡도: O(n²)
# 공간복잡도: O(1)
# 불안정 정렬

특징: 교환 횟수가 적음, 항상 O(n²)


3. 삽입 정렬 (Insertion Sort)

각 요소를 적절한 위치에 삽입하는 방식

def insertion_sort(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i - 1
 
        # key보다 큰 요소들을 오른쪽으로 이동
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]
            j -= 1
 
        arr[j + 1] = key
 
    return arr
 
# 테스트
print(insertion_sort([12, 11, 13, 5, 6]))
# [5, 6, 11, 12, 13]
 
# 시간복잡도: O(n²) - 최선 O(n)
# 공간복잡도: O(1)
# 안정 정렬

특징: 거의 정렬된 데이터에 매우 효율적, 작은 데이터에 유리


고급 정렬 알고리즘

4. 병합 정렬 (Merge Sort)

분할 정복 방식으로 배열을 나누고 합치며 정렬

def merge_sort(arr):
    if len(arr) <= 1:
        return arr
 
    # 분할
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
 
    # 병합
    return merge(left, right)
 
def merge(left, right):
    result = []
    i = j = 0
 
    # 두 배열을 비교하며 병합
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
 
    # 남은 요소 추가
    result.extend(left[i:])
    result.extend(right[j:])
 
    return result
 
# 테스트
print(merge_sort([38, 27, 43, 3, 9, 82, 10]))
# [3, 9, 10, 27, 38, 43, 82]
 
# 시간복잡도: O(n log n) - 항상 보장
# 공간복잡도: O(n)
# 안정 정렬

특징: 안정적인 O(n log n), 추가 메모리 필요


5. 퀵 정렬 (Quick Sort)

피벗을 기준으로 작은 값과 큰 값을 나누어 정렬

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
 
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
 
    return quick_sort(left) + middle + quick_sort(right)
 
# 제자리 정렬 버전
def quick_sort_inplace(arr, low, high):
    if low < high:
        pi = partition(arr, low, high)
        quick_sort_inplace(arr, low, pi - 1)
        quick_sort_inplace(arr, pi + 1, high)
 
def partition(arr, low, high):
    pivot = arr[high]
    i = low - 1
 
    for j in range(low, high):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
 
    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    return i + 1
 
# 테스트
print(quick_sort([10, 7, 8, 9, 1, 5]))
# [1, 5, 7, 8, 9, 10]
 
arr = [10, 7, 8, 9, 1, 5]
quick_sort_inplace(arr, 0, len(arr) - 1)
print(arr)  # [1, 5, 7, 8, 9, 10]
 
# 시간복잡도: 평균 O(n log n), 최악 O(n²)
# 공간복잡도: O(log n)
# 불안정 정렬

특징: 평균적으로 가장 빠름, 피벗 선택이 중요


자주 나오는 문제 유형

1. K번째 큰 수 찾기

문제: 정렬되지 않은 배열에서 K번째로 큰 수 찾기

import heapq
 
def find_kth_largest(nums, k):
    # 방법 1: 정렬 사용
    return sorted(nums, reverse=True)[k - 1]
 
    # 방법 2: 힙 사용 (더 효율적)
    # return heapq.nlargest(k, nums)[k - 1]
 
    # 방법 3: 최소 힙 사용 (공간 O(k))
    heap = nums[:k]
    heapq.heapify(heap)
 
    for num in nums[k:]:
        if num > heap[0]:
            heapq.heapreplace(heap, num)
 
    return heap[0]
 
# 테스트
print(find_kth_largest([3, 2, 1, 5, 6, 4], 2))  # 5
 
# 시간복잡도: O(n log n) 정렬, O(n log k) 힙
# 공간복잡도: O(k)

핵심: 전체 정렬 vs 부분 정렬 선택


2. 색깔별 정렬 (Sort Colors)

문제: 0, 1, 2로만 이루어진 배열을 정렬 (Dutch National Flag)

def sort_colors(nums):
    # 투 포인터 (three-way partitioning)
    low = mid = 0
    high = len(nums) - 1
 
    while mid <= high:
        if nums[mid] == 0:
            nums[low], nums[mid] = nums[mid], nums[low]
            low += 1
            mid += 1
        elif nums[mid] == 1:
            mid += 1
        else:  # nums[mid] == 2
            nums[mid], nums[high] = nums[high], nums[mid]
            high -= 1
 
    return nums
 
# 테스트
print(sort_colors([2, 0, 2, 1, 1, 0]))  # [0, 0, 1, 1, 2, 2]
 
# 시간복잡도: O(n)
# 공간복잡도: O(1)

핵심: 한 번의 순회로 O(n) 정렬


3. 배열 합병 (Merge Sorted Array)

문제: 정렬된 두 배열을 하나로 합치기

def merge_sorted_arrays(nums1, m, nums2, n):
    # 뒤에서부터 채우기
    p1 = m - 1
    p2 = n - 1
    p = m + n - 1
 
    while p1 >= 0 and p2 >= 0:
        if nums1[p1] > nums2[p2]:
            nums1[p] = nums1[p1]
            p1 -= 1
        else:
            nums1[p] = nums2[p2]
            p2 -= 1
        p -= 1
 
    # nums2의 남은 요소 복사
    nums1[:p2 + 1] = nums2[:p2 + 1]
 
    return nums1
 
# 테스트
nums1 = [1, 2, 3, 0, 0, 0]
nums2 = [2, 5, 6]
print(merge_sorted_arrays(nums1, 3, nums2, 3))
# [1, 2, 2, 3, 5, 6]
 
# 시간복잡도: O(m + n)
# 공간복잡도: O(1)

핵심: 뒤에서부터 채워 공간복잡도 O(1)


4. 회의실 배정 (Meeting Rooms)

문제: 회의 시간이 겹치지 않는지 확인

def can_attend_meetings(intervals):
    # 시작 시간 기준 정렬
    intervals.sort(key=lambda x: x[0])
 
    # 겹치는지 확인
    for i in range(1, len(intervals)):
        if intervals[i][0] < intervals[i - 1][1]:
            return False
 
    return True
 
# 테스트
print(can_attend_meetings([[0, 30], [5, 10], [15, 20]]))  # False
print(can_attend_meetings([[7, 10], [2, 4]]))             # True
 
# 시간복잡도: O(n log n)
# 공간복잡도: O(1)

핵심: 정렬 후 순차 비교


5. 최소 회의실 개수

문제: 모든 회의를 진행하기 위한 최소 회의실 개수

import heapq
 
def min_meeting_rooms(intervals):
    if not intervals:
        return 0
 
    # 시작 시간 기준 정렬
    intervals.sort(key=lambda x: x[0])
 
    # 최소 힙 (종료 시간 저장)
    heap = []
    heapq.heappush(heap, intervals[0][1])
 
    for i in range(1, len(intervals)):
        # 가장 빨리 끝나는 회의가 현재 회의 시작 전에 끝나면
        if heap[0] <= intervals[i][0]:
            heapq.heappop(heap)
 
        heapq.heappush(heap, intervals[i][1])
 
    return len(heap)
 
# 테스트
print(min_meeting_rooms([[0, 30], [5, 10], [15, 20]]))  # 2
 
# 시간복잡도: O(n log n)
# 공간복잡도: O(n)

핵심: 힙으로 종료 시간 관리


추천 연습 문제

기초

중급

고급


핵심 요약

알고리즘시간복잡도공간복잡도사용 시기
버블/선택O(n²)O(1)작은 데이터, 교육용
삽입O(n²)O(1)거의 정렬된 데이터
병합O(n log n)O(n)안정 정렬 필요 시
O(n log n)O(log n)평균적으로 가장 빠름
Python 내장O(n log n)O(n)실전에서 사용

정렬 선택 가이드

  1. Python 내장 정렬 사용

    • 대부분의 경우 sort() 또는 sorted() 사용
    • Timsort 알고리즘 (병합 + 삽입)
  2. 직접 구현이 필요한 경우

    • 특수한 비교 로직
    • 공간복잡도 제약
    • 알고리즘 이해를 위한 학습
  3. 부분 정렬만 필요한 경우

    • 힙 사용 (heapq.nlargest, heapq.nsmallest)