cv.cm

코딩 · 알고리즘

버블 정렬 알고리즘 원리, 코드, 시간 복잡도

6분 분량

핵심 답변

버블 정렬은 리스트를 반복해서 훑으며 순서가 틀린 인접한 두 요소를 교환하는 단순한 정렬 알고리즘입니다. 패스가 끝날 때마다 남은 요소 중 가장 큰 값이 거품처럼 올라가 끝의 제자리에 놓입니다. 평균과 최악의 경우 시간은 O(n²), 이미 정렬된 리스트에서는 O(n)이며, 추가 공간은 O(1)이고 안정 정렬입니다. 주로 교육용으로 쓰이고, 실제 프로그램에서는 내장 정렬을 사용합니다.

줄 정렬 열기

쉬운 말로 설명하는 원리

처음 두 요소를 비교합니다. 앞의 것이 더 크면 교환합니다. 그다음 두 번째와 세 번째, 세 번째와 네 번째를 비교하는 식으로 리스트 끝까지 갑니다. 이것이 한 번의 패스입니다. 첫 번째 패스가 끝나면 모든 비교에서 큰 값이 한 칸씩 오른쪽으로 옮겨졌으므로 가장 큰 요소가 맨 끝에 있다는 것이 보장됩니다.

정렬되지 않은 부분에 대해 패스를 반복합니다. 이 부분은 매번 한 요소씩 줄어듭니다. 한 패스에서 교환이 한 번도 일어나지 않으면 리스트가 정렬된 것이므로 일찍 멈출 수 있습니다.

패스별 단계 예제

[5, 1, 4, 2, 8]을 오름차순으로 정렬해 봅시다. 막대 뒤의 요소는 최종 위치에 놓인 것입니다.

패스비교와 교환패스 후 리스트
15↔1 교환, 5↔4 교환, 5↔2 교환, 5와 8은 유지[1, 4, 2, 5, 8] (8 확정)
21과 4 유지, 4↔2 교환, 4와 5 유지[1, 2, 4, 5, 8] (5 확정)
31과 2 유지, 2와 4 유지, 교환 없음[1, 2, 4, 5, 8] 정렬 완료, 종료

조기 종료가 없다면 네 번째 패스를 실행했을 것입니다. 조기 종료가 있으면 여기서는 세 번의 패스면 충분합니다.

파이썬 구현

def bubble_sort(items):
    """Sort a list in place, ascending. Returns the same list."""
    n = len(items)
    for end in range(n - 1, 0, -1):     # last index of the unsorted part
        swapped = False
        for i in range(end):
            if items[i] > items[i + 1]:  # strict >, keeps equal items in order
                items[i], items[i + 1] = items[i + 1], items[i]
                swapped = True
        if not swapped:                  # no swaps: already sorted
            break
    return items
print(bubble_sort([5, 1, 4, 2, 8]))     # [1, 2, 4, 5, 8]
print(bubble_sort([3, 3, 1]))           # [1, 3, 3]
print(bubble_sort([]))                  # []

안쪽 반복문은 end에서 멈춥니다. 그 뒤의 요소는 이미 제자리에 있기 때문입니다. swapped 플래그 덕분에 정렬된 입력에서 최선의 경우가 O(n)이 됩니다.

시간 복잡도와 공간 복잡도

경우비교 횟수시간이유
최선(이미 정렬됨)n − 1O(n)교환 없는 패스 한 번 후 종료
평균(무작위 순서)약 n²/2O(n²)평균적으로 교환은 약 n²/4번
최악(역순)n(n − 1)/2O(n²)모든 비교가 교환으로 이어짐
추가 공간O(1)변수 몇 개로 제자리에서 정렬

역순으로 놓인 요소가 10,000개라면 비교와 교환이 거의 5,000만 번입니다. 교환 횟수는 항상 역전(inversion), 즉 순서가 틀린 쌍의 개수와 같습니다.

버블 정렬과 다른 정렬 비교

알고리즘최선평균최악추가 공간안정 정렬
버블 정렬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)아니요
Timsort (파이썬 sorted)O(n)O(n log n)O(n log n)O(n)예

버블 정렬은 인접한 요소만, 그것도 엄밀히 순서가 틀렸을 때만 교환하므로 안정 정렬입니다. 같은 값의 요소는 서로를 추월하지 않습니다. 삽입 정렬은 복잡도와 안정성이 같지만 실제로는 쓰기 횟수가 적고 더 빠릅니다. 그래서 Timsort 같은 하이브리드 알고리즘 내부에는 버블 정렬이 아니라 삽입 정렬이 쓰입니다.

변형과 흔한 실수

  • 칵테일 셰이커 정렬은 패스마다 왼쪽에서 오른쪽, 오른쪽에서 왼쪽으로 번갈아 진행합니다. 그러면 끝에 있는 작은 요소(거북이라고 부름)가 더 빨리 앞으로 옵니다. 그래도 O(n²)입니다.
  • 콤 정렬은 일정한 간격만큼 떨어진 요소를 비교하면서 간격을 줄여 나가므로 많은 거북이를 일찍 제거합니다. 간격이 1이면 버블 정렬이 됩니다.
  • 홀짝 전치 정렬은 홀수 쌍을 모두 비교한 다음 짝수 쌍을 모두 비교하며, 병렬 정렬을 가르칠 때 쓰입니다.

버블 정렬을 쓰는 경우

실제 코드에서는 거의 쓰지 않습니다. 파이썬에서는 sorted(items)나 items.sort()를 호출하세요. 이들은 Timsort를 사용하고, O(n log n)에 동작하며, 안정 정렬입니다. 그래도 버블 정렬은 알아 둘 만합니다. 설명하기 가장 쉬운 정렬이고, 정확성 증명도 따라가기 쉬우며, 반복문, 교환, 복잡도에 대한 면접과 시험의 단골 첫 문제이기 때문입니다.

실질적인 장점은 O(n) 한 번의 패스로 리스트가 정렬되어 있는지 알 수 있다는 것 하나뿐인데, 이웃끼리 비교하는 단순한 반복문으로도 정렬 없이 확인할 수 있습니다.

자주 묻는 질문

버블 정렬은 안정 정렬인가요?

네. 왼쪽 요소가 엄밀히 더 클 때만 인접한 요소를 교환하므로, 같은 값의 요소는 원래 순서를 유지합니다.

버블 정렬의 시간 복잡도는 얼마인가요?

평균과 최악의 경우 O(n²)입니다. 교환이 없는 패스에서 조기 종료하면, 이미 정렬된 입력에서 최선의 경우는 O(n)입니다.

왜 버블 정렬이라고 부르나요?

큰 요소가 수면으로 올라가는 거품처럼 한 번에 한 칸씩 리스트의 끝으로 이동하기 때문입니다. k번째 패스가 끝나면 가장 큰 k개 요소가 제자리에 있습니다.

버블 정렬이 삽입 정렬보다 빠른가요?

아니요. 둘 다 O(n²)이지만 삽입 정렬이 보통 연산 수가 적고, 특히 거의 정렬된 데이터에서 실제로 더 빠릅니다.

버블 정렬은 추가 메모리를 쓰나요?

아니요. 제자리에서 정렬하며, 반복문 인덱스와 교환할 때의 임시 값을 위한 O(1) 추가 공간만 필요합니다.

코딩 튜토리얼 전체