언어 자료구조 알고리즘/[C++]디딤돌 자료구조와 알고리즘

2.2 거품 정렬 (Bubble Sort) [디딤돌 자료구조와 알고리즘 with C++]

언제나휴일 2016. 4. 4. 10:14
반응형

2.2 거품 정렬(Bubble Sort)

 

이번에는 반복적인 방법으로 해결하는 거품 정렬(Bubble Sort) 알고리즘을 살펴볼게요.

 

거품 정렬은 앞에서부터 이웃하는 원소의 값을 비교하여 위치를 교환하는 것을 반복해요. 이를 끝까지 수행하면 제일 큰 값이 맨 뒤에 위치합니다. 그리고 정렬할 개수를 1 줄인 후에 다시 반복해요. 정렬할 개수가 1일 때까지 반복하면 정렬 작업이 끝나요.

거품 정렬(base:배열의 시작 주소, n: 원소 개수, compare:비교 논리)

    반복(i:=n->1)

        반복(j:=1->i)

            조건(compare(base[j-1], base[j]) > 0)

                교환(base[j-1],base[j])

:

2 9 4 1 5 6 8 3 7       (정렬 전, n:9)

2 9 4 1 5 6 8 3 7       (i:9, j:1)

2 9 4 1 5 6 8 3 7       (i:9, j:2)

2 4 9 1 5 6 8 3 7       (i:9, j:2) - 교환

2 4 9 1 5 6 8 3 7       (i:9, j:3)

2 4 1 9 5 6 8 3 7       (i:9, j:3) - 교환

2 4 1 9 5 6 8 3 7       (i:9, j:4)

2 4 1 5 9 6 8 3 7       (i:9, j:4) - 교환

2 4 1 5 9 6 8 3 7       (i:9, j:5)

2 4 1 5 6 9 8 3 7       (i:9, j:5) - 교환

2 4 1 5 6 9 8 3 7       (i:9, j:6)

2 4 1 5 6 8 9 3 7       (i:9, j:6) - 교환

2 4 1 5 6 8 9 3 7       (i:9, j:7)

2 4 1 5 6 8 3 9 7       (i:9, j:7) - 교환

2 4 1 5 6 8 3 9 7       (i:9, j:8)

2 4 1 5 6 8 3 7 9       (i:9, j:8) - 교환, 1회전 완료

2 4 1 5 6 8 3 7 9       (i:8, j:1)

2 4 1 5 6 8 3 7 9       (i:8, j:2)

2 1 4 5 6 8 3 7 9       (i:8, j:2) - 교환

2 1 4 5 6 8 3 7 9       (i:8, j:3)

2 1 4 5 6 8 3 7 9       (i:8, j:4)

2 1 4 5 6 8 3 7 9       (i:8, j:5)

2 1 4 5 6 8 3 7 9       (i:8, j:6)

2 1 4 5 6 3 8 7 9       (i:8, j:6) - 교환

2 1 4 5 6 3 8 7 9       (i:8, j:7)

2 1 4 5 6 3 7 8 9       (i:8, j:7) - 교환, 2회전 완료

2 1 4 5 6 3 7 8 9       (i:7, j:1)

1 2 4 5 6 3 7 8 9       (i:7, j:1) - 교환

...생략...

 

2.2.1 거품 정렬 알고리즘 분석

이번에는 거품 정렬 알고리즘의 타당성 및 수행 속도를 계산해 보기로 해요.

거품 정렬(base:배열의 시작 주소, n: 원소 개수, compare:비교 논리)

    반복(i:=n->1)

        반복(j:=1->i)

            조건(compare(base[j-1], base[j]) > 0)

                교환(base[j-1],base[j])

 

거품 정렬 알고리즘을 보면 반복문 내부에 반복문이 있는 구조입니다. 먼저 내부의 반복문에서 하는 일이 무엇인지 알아봅시다.

 

거품 정렬 알고리즘의 내부 반복문에서는 j값을 1부터 시작하여 i까지 순차적으로 증가하며 작업을 수행하죠. 따라서 루프 변성은 j값이 변하는 것이예요. 그리고 배열의 j번째 요소와 j번째 앞의 요소와 비교하여 앞쪽 요소가 작으면 위치를 교환합니다. 이처럼 수행하면 배열의 맨 앞의 요소부터 j번째 요소 중에서 제일 큰 값은 j번째 요소가 되죠. 따라서 루프 불변성은 배열의 맨 앞의 요소부터 j번째 요소 중에서 제일 큰 값은 j번째에 있다는 것이예요.

 

루프 변성인 j값이 i전까지 변하는 특징과 루프 불변성인 맨 앞의 요소부터 j번째 요소 중에 제일 큰 값은 j번째 있다는 특징으로 인해 반복문을 수행하면 배열의 맨 앞에서 i-1 번째 요소 중에 제일 큰 값은 i-1 번째 요소라고 말할 수 있어요.

 

이번에는 내부 반복문을 감싸고 있는 외부 반복문을 살펴보아요. i max에서 1보다 크면 1씩 감소하고 있어요. 내부 반복문을 수행하면 제일 큰 값이 i-1번째로 이동하기 때문에 1회전에서 최대값은 n-1번째인 마지막에 배치합니다. 다시 i1 감소한 후에 내부 반복문을 수행하면 그 다음 최대값이 n-2번째에 배치합니다. 따라서 i번째 이후의 원소들은 언제나 정렬 상태를 유지하고 i번 앞쪽의 요소보다 큰 값을 갖고 있음을 알 수 있어요. 이러한 특징이 루프 불변성이예요.

 

결국 루프 변성이 i1까지 가므로 루프 불변성 i 이후의 원소는 정렬 상태이고 앞쪽의 요소보다 큰 값을 갖는다는 규칙에 의해 위 알고리즘이 타당함을 알 수 있어요.

 

이번에는 거품 정렬 알고리즘 성능을 분석합시다.

 

거품 정렬의 내부 반복문의 수행 시간을 S(i)라고 가정할게요. 내부 반복문은 j1에서 i이전까지 순차적으로 증가하므로 비교를 i-1, 교환을 최악일 때 i-1번 한다고 볼 수 있어요. 따라서 S(i)= i-1이죠.

 

외부 반복문은 in에서 1로 역순으로 감소하면서 내부 반복문을 수행합니다. 수행 시간을 T(n)이라 가정하면 T(n) = S(n) + S(n-1) + S(n-2) + ... +1 이죠.

 

따라서 T(n) = S(n) + S(n-1) + ... +S(2) + S(1) = n-1 + n-2 + .... + 1 + 0

 

따라서 T(n)= n(n-1)/2 이고 점근식 표기에서 높은 차수의 항만 남고 상수를 버리므로 O(n^2)라고 할 수 있습니다. 거품 정렬도 순차 정렬처럼 O(n^2)이네요.

 

2.2.2 거품 정렬 알고리즘 구현

이번에는 거품 정렬 알고리즘을 구현해 보기로 해요.

거품 정렬(base:배열의 시작 주소, n: 원소 개수, compare:비교 논리)

    반복(i:=n->1)

        반복(j:=1->i)

            조건(compare(base[j-1], base[j]) > 0)

                교환(base[j-1],base[j])

 

참고로 2.1.2에서 공통으로 사용할 코드를 이용하세요.

template <typename data, typename compare>

//거품 정렬(base:배열의 시작 주소, n: 원소 개수, compare:비교 논리)

void bubble_sort(data *base, size_t n, compare com)

{

    for(size_t i=n; i>1; i--) //반복(i:=n->1)

    {

        for(size_t j=1; j<i; j++) //반복(j:=1->i)

        {

            if(com(base[j-1],base[j])>0)//조건(compare(base[j-1], base[j]) > 0)

            {

                swap(base[j-1], base[j]);//교환(base[j-1],base[j])

            }

        }

    }

}

 

테스트 코드도 2.1.3에서 작성한 코드에서 sequential_sort 부분을 bubble_sort로 변경하세요.

#define MAX_DATA 1000

 

int main()

{

    Member *base[MAX_DATA];

버블 정렬이 잘 동작하는지 10개 원소를 번호 순으로 정렬하여 확인하고 이름 순으로 정렬하여 확인하세요.

    MakeRandomMembers(base,10);

    cout<<"정렬 전"<<endl;

    ViewMembers(base,10);

    bubble_sort(base,10,CompareByNum);

    cout<<"번호로 정렬 후"<<endl;

    ViewMembers(base,10);

    bubble_sort(base,10,CompareByName);

    cout<<"이름으로 정렬 후"<<endl;

    ViewMembers(base,10);

    RemoveMembers(base,10);

 

그리고 MAX_DATA 개일 때 수행 속도와 MAX_DATA/10 개일 때 수행 속도를 비교해 보세요.

    clock_t st,et;

 

    MakeRandomMembers(base,MAX_DATA);

    cout<<"수행 성능 테스트1 자료 개수:"<<MAX_DATA<<endl;

    st = clock();   

    bubble_sort(base,MAX_DATA,CompareByName);   

    et=clock();

    cout<<"수행 속도:"<<et-st<<endl;

    RemoveMembers(base,MAX_DATA);

 

    MakeRandomMembers(base,MAX_DATA/10);

    cout<<"수행 성능 테스트2 자료 개수:"<<MAX_DATA/10<<endl;

    st = clock();

    MakeRandomMembers(base,MAX_DATA/10);

    bubble_sort(base,MAX_DATA/10,CompareByName);

    et=clock();

    cout<<"수행 속도:"<<et-st<<endl;

    RemoveMembers(base,MAX_DATA/10);

    return 0;

}

 

다음은 실행 결과예요.

▷실행 결과

정렬 전

0000757147,이름0167851000

0301413356,이름0336971124

0659598368,이름0160567225

0391749387,이름0004890851

0035766290,이름0026239572

0473038164,이름0000597006

0003615544,이름0326051436

0392289610,이름0118341522

0170427798,이름0037215528

0675016433,이름0168544290

번호로 정렬 후

0000757147,이름0167851000

0003615544,이름0326051436

0035766290,이름0026239572

0170427798,이름0037215528

0301413356,이름0336971124

0391749387,이름0004890851

0392289610,이름0118341522

0473038164,이름0000597006

0659598368,이름0160567225

0675016433,이름0168544290

이름으로 정렬 후

0473038164,이름0000597006

0391749387,이름0004890851

0035766290,이름0026239572

0170427798,이름0037215528

0392289610,이름0118341522

0659598368,이름0160567225

0000757147,이름0167851000

0675016433,이름0168544290

0003615544,이름0326051436

0301413356,이름0336971124

수행 성능 테스트1 자료 개수:1000

수행 속도:4357

수행 성능 테스트2 자료 개수:100

수행 속도:36

 

거품 정렬도 결과를 보면 자료 개수가 MAX_DATA일 때 MAX_DATA/10일 때보다 100배 정도 느린 것을 알 수 있습니다. 앞에서 알고리즘 성능을 분석할 때 O(n^2)와 비슷하죠.

반응형