Post

[알고리즘] 버블 정렬 (Bubble Sort)

[알고리즘] 버블 정렬 (Bubble Sort)

1. 버블 정렬

버블 정렬은 정렬 알고리즘 중 하나로 원소의 이동이 거품이 수면으로 올라오는 듯한 모습으로 보이기 때문에 지어진 이름이다. 코드가 단순하기 때문에 간단한 정렬 알고리즘으로 한 번쯤 배워볼 만하다.


2. 버블 정렬 성능

평균 시간복잡도$O(N^2)$
최선 시간복잡도$O(N^2)$, $O(N)$
최악 시간복잡도$O(N^2)$
공간복잡도$O(1)$

$N$ 개의 원소에 대한 버블 정렬은 기본적으로 2중 반복문을 통해 정렬을 수행하여 시간복잡도가 $O(N^2)$ 이다. 원소들이 처음부터 정렬되어 있을 경우 비교 작업을 수행하지 않도록 최적화하면 최선 시간복잡도는 $O(N)$ 까지도 가능하다. 오름차순으로 정렬하고자 할 때 원소들이 역순으로 주어지면 비교마다 교환이 계속 반복돼서 최악으로 동작한다.

버블 정렬은 제자리 정렬(In-place Sort)이기 때문에 스왑을 위한 변수 하나의 공간만 필요하여 공간복잡도는 $O(1)$ 이다.

버블 정렬은 동일한 가중치의 두 수에 대해 정렬 후에도 기존 순서를 보장하는 안정 정렬(Stable Sort)이다.


3. 버블 정렬 진행 과정

버블 정렬은 기본적으로 두 수($a$, $b$)를 선택한 뒤, 만약 그 두 수가 정렬되었다면 놔두고 아니라면 두 수를 바꾸는 방식으로 진행된다. 오름차순으로 정렬할 때는 $a < b$, 내림차순이라면 $a > b$ 여야 정렬된 것으로 판단한다. 이를 주어진 수에 대해 반복한다.

주어진 수들이 $[4, 1, 8, 3, 6, 10, 8, 4]$ 일 경우 오름차순 버블 정렬 과정은 아래와 같이 진행된다.

먼저 처음 두 수가 오름차순인지 비교한다.

오름차순이 아니므로 두 수를 바꾼다.

한 칸 이동한 뒤 다시 두 수가 오름차순인지 비교한다.

오름차순이므로 두 수를 바꾸지 않는다.

한 칸 이동한 뒤 다시 두 수가 오름차순인지 비교한다.

오름차순이 아니므로 두 수를 바꾼다.

한 칸 이동한 뒤 다시 두 수가 오름차순인지 비교한다.

오름차순이 아니므로 두 수를 바꾼다.

한 칸 이동한 뒤 다시 두 수가 오름차순인지 비교한다.

오름차순이므로 두 수를 바꾸지 않는다.

한 칸 이동한 뒤 다시 두 수가 오름차순인지 비교한다.

오름차순이 아니므로 두 수를 바꾼다.

한 칸 이동한 뒤 다시 두 수가 오름차순인지 비교한다.

오름차순이 아니므로 두 수를 바꾼다.

주어진 수의 끝에 도달했으며 교환 과정을 통해 주어진 수 중 가장 큰 수인 $10$ 이 끝에 위치하게 됐다. 하나의 수를 정렬하는데 성공했다.

다시 처음 두 수가 오름차순인지 비교한다.

오름차순이므로 두 수를 바꾸지 않는다.

한 칸 이동한 뒤 다시 두 수가 오름차순인지 비교한다.

오름차순이 아니므로 두 수를 바꾼다.

한 칸 이동한 뒤 다시 두 수가 오름차순인지 비교한다.

오름차순이므로 두 수를 바꾸지 않는다.

한 칸 이동한 뒤 다시 두 수가 오름차순인지 비교한다.

오름차순이므로 두 수를 바꾸지 않는다.

한 칸 이동한 뒤 다시 두 수가 오름차순인지 비교한다.

오름차순이므로 두 수를 바꾸지 않는다. 동일한 두 수에 대해 두 수를 바꾸지 않기 때문에 3번의 $8$ 이 7번의 $8$ 보다 항상 앞에 위치하게 되고 이를 통해 안정 정렬이 된다.

한 칸 이동한 뒤 다시 두 수가 오름차순인지 비교한다.

오름차순이 아니므로 두 수를 바꾼다.

주어진 수의 끝에는 이미 가장 큰 $10$ 이 있으므로 오름차순인지 더 비교하지 않아도 된다. $10$ 앞에 주어진 수 중 두 번째로 큰 수인 $8$ 이 위치하게 됐다. 두 개의 수 정렬하는데 성공했다.

다시 처음 두 수부터 다음 정렬은 아래와 같이 이어진다.

다시 처음 두 수부터 다음 정렬은 아래와 같이 이어진다.

다시 처음 두 수부터 다음 정렬은 아래와 같이 이어진다.

다시 처음 두 수부터 다음 정렬은 아래와 같이 이어진다.

다시 처음 두 수부터 다음 정렬은 아래와 같이 이어진다.

$N$ 개의 수 중에서 $N-1$ 개를 정렬했다. 남은 수는 주어진 수 중에서 가장 작은 수면서 이미 가장 앞에 위치하게 되므로 버블 정렬을 종료한다. 이렇게 버블 정렬을 통해 주어진 수들을 오름차순으로 정렬하는데 성공했다.

버블 정렬의 진행과정을 보면 버블 정렬은 정렬이 안된 구간 중 가장 큰 수를 찾아서 이동시키는 과정을 반복하는 것을 볼 수 있다.


4. 버블 정렬 코드

버블 정렬 [Java]

$100,000$ 개의 임의의 수들에 대한 정렬에서 약 5.5초정도 소요됐다.(Macbook M3 Air, JDK 21)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void bubbleSort(int[] arr) {
    int N = arr.length;

    for (int i = 0; i < N - 1; i++) {  // N-1개만 정렬하면 됨
        for (int j = 0; j < N - 1 - i; j++) {  // 정렬이 안된 구간만 탐색
            if (arr[j] > arr[j + 1]) {

                // swap
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

스왑 체크로 최적화 [Java]

$100,000$ 개의 임의의 수들에 대한 정렬에서 약 5.4초정도 소요됐다.(Macbook M3 Air, JDK 21)

거의 정렬된 상태로 주어지는게 아니면 오히려 체크 로직의 오버헤드가 큰 것 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void optimizedBubbleSort(int[] arr) {
    int N = arr.length;

    for (int i = 0; i < N - 1; i++) {
        boolean swapped = false;  // 교환 여부

        for (int j = 0; j < N - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {

                // swap
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
                swapped = true;
            }
        }

        // 한번도 교환된 적이 없으면 남은 구간이 전부 정렬된 상태이므로 종료
        if (!swapped) {
            break;
        }
    }
}

Ref


This post is licensed under CC BY 4.0 by the author.