버블 정렬 (Bubble Sort)

“정렬 알고리즘을 처음 배운다면 어디서부터 시작해야 할까요?”

저는 처음 알고리즘을 배울 때 복잡한 퀵 정렬부터 시작했다가 포기한 경험이 있습니다. 하지만 버블 정렬로 다시 시작하니, “아하! 정렬이란 게 이런 거구나!”를 깨달을 수 있었습니다.

버블 정렬은 물 속의 거품(bubble)이 위로 떠오르는 것처럼 큰 값들이 배열의 끝으로 이동하는 알고리즘입니다.

왜 버블 정렬을 배워야 할까요?

1. 정렬의 기본 개념 이해

버블 정렬은 정렬의 핵심 아이디어를 보여줍니다.

  • 비교: 두 원소를 비교한다
  • 교환: 순서가 잘못되었으면 바꾼다
  • 반복: 정렬될 때까지 계속한다

이 세 가지 개념만 이해하면 모든 정렬 알고리즘을 이해할 수 있습니다.

2. 디버깅하기 쉬움

각 단계를 눈으로 확인할 수 있어서 배우기 좋습니다.

[5, 2, 8, 1, 9]  시작

[2, 5, 8, 1, 9]  5와 2 비교 후 교환
[2, 5, 8, 1, 9]  5와 8 비교 (교환 X)
[2, 5, 1, 8, 9]  8과 1 비교 후 교환
[2, 5, 1, 8, 9]  8과 9 비교 (교환 X)

첫 번째 패스 완료! 9가 맨 뒤로 이동 ✅

3. 면접에서 자주 나옴

“정렬 알고리즘을 하나 설명해보세요”라는 질문에 버블 정렬이 가장 설명하기 쉽습니다.

어떻게 동작하나요?

시각적 설명

배열을 물컵이라고 생각해보세요. 숫자는 거품이고, 큰 거품일수록 빨리 떠오릅니다.

패스 1: 가장 큰 거품이 맨 위로
[5, 2, 8, 1, 9]
 ↓  ↓
[2, 5, 8, 1, 9]  (5 > 2, 교환)
    ↓  ↓
[2, 5, 8, 1, 9]  (5 < 8, 유지)
       ↓  ↓
[2, 5, 1, 8, 9]  (8 > 1, 교환)
          ↓  ↓
[2, 5, 1, 8, 9]  (8 < 9, 유지) → 9가 제자리! ✅

패스 2: 두 번째로 큰 거품이 두 번째 위치로
[2, 5, 1, 8, 9]
 ↓  ↓
[2, 5, 1, 8, 9]  (2 < 5, 유지)
    ↓  ↓
[2, 1, 5, 8, 9]  (5 > 1, 교환)
       ↓  ↓
[2, 1, 5, 8, 9]  (5 < 8, 유지) → 8이 제자리! ✅

... 반복 ...

최종: [1, 2, 5, 8, 9] ✅

단계별 설명

1단계: 인접한 두 원소를 비교

if (arr[j] > arr[j + 1]) {
  // 왼쪽이 오른쪽보다 크면...
}

2단계: 순서가 잘못되었으면 교환

[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];

3단계: 배열 끝까지 반복

for (let j = 0; j < arr.length - 1; j++) {
  // 처음부터 끝까지
}

4단계: 전체 과정을 배열 크기만큼 반복

for (let i = 0; i < arr.length; i++) {
  // n번 반복
}

코드 구현

JavaScript 기본 구현

function bubbleSort(arr) {
  const n = arr.length;

  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        // 두 원소 교환
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }

  return arr;
}

// 사용 예시
const numbers = [5, 2, 8, 1, 9];
console.log(bubbleSort(numbers));  // [1, 2, 5, 8, 9]

TypeScript 최적화 버전

function bubbleSortOptimized<T>(arr: T[]): T[] {
  const n = arr.length;
  const result = [...arr];  // 원본 보호

  for (let i = 0; i < n; i++) {
    let swapped = false;  // 교환 발생 체크

    // 이미 정렬된 부분은 제외 (n - i - 1)
    for (let j = 0; j < n - i - 1; j++) {
      if (result[j] > result[j + 1]) {
        [result[j], result[j + 1]] = [result[j + 1], result[j]];
        swapped = true;
      }
    }

    // 교환이 없었다면 이미 정렬됨
    if (!swapped) break;
  }

  return result;
}

최적화 포인트:

  1. 조기 종료: 교환이 없으면 정렬 완료
  2. 범위 축소: 이미 정렬된 뒷부분 제외
// ❌ 최적화 전: 항상 n² 비교
for (let j = 0; j < n - 1; j++)

// ✅ 최적화 후: 이미 정렬된 부분 제외
for (let j = 0; j < n - i - 1; j++)

// ✅ 추가 최적화: 조기 종료
if (!swapped) break;  // 이미 정렬됨!

실행 과정 시뮬레이션

// 디버깅용 버전
function bubbleSortDebug(arr) {
  console.log('시작:', arr);
  const n = arr.length;

  for (let i = 0; i < n; i++) {
    console.log(`\n패스 ${i + 1}:`);
    let swapped = false;

    for (let j = 0; j < n - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        console.log(`  교환: [${arr[j]}, ${arr[j + 1]}] → [${arr[j + 1]}, ${arr[j]}]`);
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        swapped = true;
      }
    }

    console.log(`  결과: [${arr}]`);
    if (!swapped) {
      console.log(`  ✅ 정렬 완료! (조기 종료)`);
      break;
    }
  }

  return arr;
}

// 실행해보기
bubbleSortDebug([5, 2, 8, 1, 9]);

출력:

시작: [5, 2, 8, 1, 9]

패스 1:
  교환: [5, 2] → [2, 5]
  교환: [8, 1] → [1, 8]
  결과: [2, 5, 1, 8, 9]

패스 2:
  교환: [5, 1] → [1, 5]
  결과: [2, 1, 5, 8, 9]

패스 3:
  교환: [2, 1] → [1, 2]
  결과: [1, 2, 5, 8, 9]

패스 4:
  결과: [1, 2, 5, 8, 9]
  ✅ 정렬 완료! (조기 종료)

시간/공간 복잡도

시간 복잡도

케이스 복잡도 설명
최선 O(n) 이미 정렬됨 (최적화 버전)
평균 O(n²) 무작위 배열
최악 O(n²) 역순 정렬

왜 O(n²)일까요?

for (let i = 0; i < n; i++) {           // n번
  for (let j = 0; j < n - i - 1; j++) { // n-1, n-2, ..., 1번
    // 비교 및 교환
  }
}

총 비교 횟수: (n-1) + (n-2) + ... + 1 = n(n-1)/2 ≈ n²

실제 숫자로 보면:

  • n=10: 약 45번 비교
  • n=100: 약 4,950번 비교
  • n=1000: 약 499,500번 비교

공간 복잡도: O(1)

추가 메모리를 거의 사용하지 않습니다 (in-place 정렬).

// ❌ 나쁜 예: 새 배열 생성 → O(n) 공간
const sorted = [...arr].sort();

// ✅ 좋은 예: 제자리 교환 → O(1) 공간
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];

장단점

✅ 장점

1. 구현이 매우 간단

// 10줄로 끝!
for (let i = 0; i < n; i++) {
  for (let j = 0; j < n - 1; j++) {
    if (arr[j] > arr[j + 1]) {
      [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
    }
  }
}

2. 안정 정렬 (Stable Sort)

같은 값의 순서가 유지됩니다.

const students = [
  { name: 'Alice', score: 80 },
  { name: 'Bob', score: 80 },
  { name: 'Charlie', score: 90 }
];

// 점수로 정렬 후에도 Alice가 Bob보다 앞에 있음

3. 추가 메모리 불필요

배열 안에서 직접 교환하므로 공간 효율적입니다.

❌ 단점

1. 느림 (O(n²))

// 100개 원소를 정렬하면...
// 버블 정렬: ~4,950번 비교
// 퀵 정렬: ~664번 비교

// 약 7.5배 차이!

2. 실무에서 사용 안 함

// ❌ 절대 이렇게 쓰지 마세요
bubbleSort(largeArray);

// ✅ 내장 정렬 사용
largeArray.sort();

3. 작은 배열에서도 비효율적

// 10개 정도는 삽입 정렬이 더 빠름
const small = [5, 2, 8, 1, 9];
insertionSort(small);  // 더 효율적

언제 사용할까요?

✅ 사용하는 경우

1. 교육 목적

// 알고리즘 수업 첫 시간
console.log('정렬이란 이런 거예요!');

2. 매우 작은 배열 (< 10개)

const tiny = [3, 1, 2];
// 이 정도는 버블 정렬도 괜찮음

3. 거의 정렬된 배열

const almostSorted = [1, 2, 3, 5, 4];  // 마지막만 안 맞음
// 한 번의 패스로 끝! O(n)

❌ 사용하지 않는 경우

1. 실무 프로젝트

// ❌ 절대 금지
const users = await fetchUsers();  // 10,000개
bubbleSort(users);

// ✅ 항상 이렇게
users.sort((a, b) => a.name.localeCompare(b.name));

2. 대용량 데이터

// 100,000개 이상이면 버블 정렬은 재앙
const bigData = generateData(100000);
// 퀵 정렬, 병합 정렬 사용!

실수하기 쉬운 함정

함정 1: 범위 초과

// ❌ 잘못된 코드
for (let j = 0; j < n; j++) {  // n까지 가면 arr[n]은 undefined!
  if (arr[j] > arr[j + 1]) {
    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
  }
}

// ✅ 올바른 코드
for (let j = 0; j < n - 1; j++) {  // n-1까지만

함정 2: 원본 배열 변경

// ❌ 원본이 변경됨
const original = [5, 2, 8];
bubbleSort(original);
console.log(original);  // [2, 5, 8] - 변경됨!

// ✅ 원본 보호
const original = [5, 2, 8];
const sorted = bubbleSort([...original]);
console.log(original);  // [5, 2, 8] - 안전!

함정 3: 무한 루프 위험

// ❌ 조건이 잘못되면 무한 루프
for (let i = 0; i <= n; i++) {  // i < n이어야 함
  for (let j = 0; j <= n - 1; j++) {  // j < n-1이어야 함

다른 정렬과 비교

vs 선택 정렬

// 버블 정렬: 인접한 원소 비교
[5, 2, 8, 1]  [2, 5, 1, 8]  ...

// 선택 정렬: 최소값을 찾아 앞으로
[5, 2, 8, 1]  [1, 2, 8, 5]  ...

차이점:

  • 버블: 교환이 많음 → 느림
  • 선택: 교환이 적음 → 약간 빠름

vs 삽입 정렬

// 버블: 끝에서부터 정렬
[5, 2, 8, 1, 9]  [..., 8, 9]  [..., 2, 8, 9]

// 삽입: 앞에서부터 정렬
[5, 2, 8, 1, 9]  [2, 5, ...]  [2, 5, 8, ...]

차이점:

  • 버블: 매번 끝까지 순회
  • 삽입: 필요한 만큼만 → 더 빠름

연습 문제

1. 기본 구현

// 버블 정렬을 직접 구현해보세요
function bubbleSort(arr) {
  // 여기에 코드 작성
}

console.log(bubbleSort([5, 2, 8, 1, 9]));
// 기대 결과: [1, 2, 5, 8, 9]

2. 역순 정렬

// 큰 것부터 작은 순서로 정렬
function bubbleSortDesc(arr) {
  // 힌트: 비교 조건만 바꾸면 됩니다
}

console.log(bubbleSortDesc([5, 2, 8, 1, 9]));
// 기대 결과: [9, 8, 5, 2, 1]

3. 객체 배열 정렬

// 나이순으로 정렬
const people = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 20 },
  { name: 'Charlie', age: 30 }
];

function bubbleSortByAge(arr) {
  // 여기에 코드 작성
}

참고 자료

공식 문서 및 표준

학습 자료

관련 문서

책 및 논문

  • Introduction to Algorithms (CLRS) - Section 2.2 Bubble Sort
  • The Art of Computer Programming, Vol. 3 - Donald Knuth, Sorting and Searching
  • Sorting and Searching (Wikipedia) - 정렬 알고리즘 역사와 비교

실습 플랫폼

마치며

버블 정렬은 실무에서는 쓰지 않지만, 학습에는 최고입니다.

이 알고리즘으로 정렬의 기본을 익혔다면, 이제 더 효율적인 알고리즘으로 넘어갈 준비가 된 것입니다!

다음 단계

같은 카테고리 (정렬):

다음 레벨 (중급):

관련 개념:

🚀 추천 학습 순서: 버블 정렬 → 이진 탐색 → DFS/BFS → 동적 프로그래밍

댓글