버블 정렬 (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;
}
최적화 포인트:
- 조기 종료: 교환이 없으면 정렬 완료
- 범위 축소: 이미 정렬된 뒷부분 제외
// ❌ 최적화 전: 항상 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) {
// 여기에 코드 작성
}
참고 자료
공식 문서 및 표준
- ECMAScript® 2023 - Array.prototype.sort - JavaScript 정렬의 공식 명세
- MDN Web Docs - Array.prototype.sort() - 브라우저 내장 정렬 문서
- V8 Blog - Array.sort - Chrome의 정렬 알고리즘 분석
학습 자료
- VisuAlgo - Sorting Algorithms - 버블 정렬 시각화 및 비교
- Big-O Cheat Sheet - 시간/공간 복잡도 비교표
- Khan Academy - Algorithms - 알고리즘 기초 강의
관련 문서
- 난이도 가이드 - 왜 버블 정렬이 초급인지 이해하기
- 알고리즘 목록 - 전체 알고리즘 학습 로드맵
- 정렬 알고리즘 구현 - TypeScript 전체 구현 코드
책 및 논문
- 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) - 정렬 알고리즘 역사와 비교
실습 플랫폼
- LeetCode - Sort Array - 정렬 구현 문제
- HackerRank - Sorting - 정렬 연습 문제
- Codewars - Sorting Katas - 정렬 도전 과제
마치며
버블 정렬은 실무에서는 쓰지 않지만, 학습에는 최고입니다.
이 알고리즘으로 정렬의 기본을 익혔다면, 이제 더 효율적인 알고리즘으로 넘어갈 준비가 된 것입니다!
다음 단계
같은 카테고리 (정렬):
다음 레벨 (중급):
관련 개념:
🚀 추천 학습 순서: 버블 정렬 → 이진 탐색 → DFS/BFS → 동적 프로그래밍
댓글