6.2.2 퀵 정렬 알고리즘 구현

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

퀵 정렬(base:컬렉션,n: 원소 개수, compare:비교 논리)

조건(n<=1)

종료

피벗을 선택한다.

피벗과 맨 앞의 요소를 교환한다.

    big:=0

    small:=n

반복(big<small)

반복(big:=big+1; big<n; big:=big+1)

조건(compare(base[0],base[big])<0)

루프 탈출

반복(small:small-1;small>0;small:small-1)

조건(compare(base[0],base[small])>0)

루프 탈출

조건(big<small)

교환(base [big], base [small])

교환(base [0], base [small])

퀵 정렬(base,small, compare)

퀵 정렬(base+big, n-big, compare)

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

template <typename data, typename compare>
//퀵 정렬(base:배열의 시작 주소, n: 원소 개수, compare:비교 논리)
void quick_sort(data *base, size_t n, compare com)
{

퀵 정렬의 탈출 조건은 정렬할 원소 개수가 1보다 작거나 같을 때입니다.

    if(n<=1) //조건(n<=1)
    {
        return; //종료
    }

피벗을 선택합시다. 여기에서는 맨 앞, 중간, 마지막 원소 중에 중간 값을 갖는 원소를 피벗으로 선택할 것입니다.

    //피벗 선택
    int pivot = 0; //피벗의 위치를 기억하기 위한 변수
    //먼저 맨 앞과 맨 마지막 원소를 비교합시다.
    if(com(base[0],base[n-1])>0)
    {
        //맨 앞의 원소가 마지막 원소보다 큰 것은 알았습니다. 
        //다시 맨 앞과 중간에 있는 원소를 비교합시다.
        if(com(base[0],base[n/2])>0) 
        //base[0]이 제일 큰 값, 이 조건인 거짓일 때는 0번째 원소가 중간 값
        {
            //맨 앞의 원소가 마지막 원소와 중간에 있는 원소보다 큰 것을 알았습니다. 
            //이제 맨 앞과 중간에 있는 원소 중에 큰 값이 피벗입니다.
            if(com(base[n/2],base[n-1])>0) //둘 중에 큰 값이 중간 값
            {
                //중간에 있는 원소가 더 크므로 중간 값은 중간에 있는 원소입니다.
                pivot = n/2;
            }
            else
            {
                //맨 뒤에 있는 원소가 더 크므로 중간 값은 맨 뒤에 있는 원소입니다.
                pivot = n-1;
            }
        }
    } 
    else //base[n-1]이 base[0]보다 크거나 같음
    { 
        //맨 앞의 원소가 마지막 원소보다 크지 않습니다. 
        //중간에 있는 원소와 마지막 원소를 비교하세요. 
        if(com(base[n/2],base[n-1])>0)
        {
            //중간에 있는 원소가 크므로 맨 뒤에 있는 원소가 중간 값입니다.
            pivot = n-1; //n-1번째 원소가 중간 값
        } 
        else//n-1번째 원소가 제일 큰 값
        { 
            //맨 뒤에 원소가 맨 앞 원소와 중간에 있는 원소보다 작지 않다는 것을 알았습니다. 
            //중간에 있는 원소와 맨 앞의 원소를 비교하세요.
            if(com(base[n/2],base[0])>0)//이 조건인 거짓일 때는 0번째 원소가 중간 값
            {
                //중간에 있는 원소가 맨 앞의 원소보다 크므로 중간 값은 중간에 있는 원소입니다.
                pivot = n/2;//n/2가 중간 값
            }
        }
    }

피벗을 선택하였으면 맨 앞의 원소와 피벗에 있는 요소를 교환하세요.

    swap(base[0],base[pivot]);//피벗과 맨 앞의 요소 교환

이제 피벗을 중심으로 큰 값은 왼쪽으로 작은 값들은 오른쪽으로 배치할 것입니다. 이를 위해 앞에서부터 피벗보다 큰 값을 찾고 뒤에서부터 피벗보다 작은 값을 찾는 과정을 반복할 것입니다. 만약 큰 값이 작은 값보다 앞에 있으면 둘을 교환하는 것을 반복할 거예요.

    size_t big=0, small=n; //피벗보다 큰 값과 작은 값의 위치를 찾기 위한 변수     
    while(big<small)//반복(big<small)
    { 
        //순차적으로 피벗과 비교하면서 더 큰 값을 만나면 반복문을 탈출하세요.
        for(big++; big<n; big++)//반복(big:=big+1; big<n; big:=big+1)
        {
            if(com(base[0],base[big])<0)//조건(compare(base[0],base[big])<0)
            {
                break;//루프 탈출
            }
        } 
        //역순으로 피벗과 비교하면서 더 작은 값을 만나면 반복문을 탈출하세요.
        for(small--; small>0; small--)//반복(small:small-1;small>0;small:small-1)
        {
            if(com(base[0],base[small])>0)//조건(compare(base[0],base[small])>0)
            {
                break;//루프 탈출
            }
        }
        //큰 값이 있는 위치가 작은 값이 있는 위치보다 앞이면 둘은 교환하세요.
        if(big<small)//조건(big<small)
        {
            swap(base[big],base[small]); //교환(base [big], base [small])            
        }
    }

이제 피벗을 중심으로 작은 값들은 왼쪽에 큰 값은 오른쪽에 배치하였습니다. 작은 값들 중에 맨 뒤에 있는 요소와 피벗을 교환하세요.

    swap(base[0],base[small]);//교환(base [0], base [small])

재귀함수로 피벗보다 작은 값들이 있는 배열을 정렬합니다.

    quick_sort(base,small,com);//퀵 정렬(base,small, compare)

재귀함수로 피벗보다 큰 값들이 있는 배열을 정렬합니다.

    quick_sort(base+big,n-big,com);//퀵 정렬(base+big, n-big, compare)
}

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

#define MAX_DATA 1000

int main()
{
    Member *base[MAX_DATA];
    //퀵 정렬이 잘 동작하는지 10개 원소를 
    //번호 순으로 정렬하여 확인하고 
    //이름 순으로 정렬하여 확인하세요.
    MakeRandomMembers(base,10);
    cout<<"정렬 전"<<endl;
    ViewMembers(base,10);
    quick_sort(base,10,CompareByNum);
    cout<<"번호로 정렬 후"<<endl;
    ViewMembers(base,10);
    quick_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();    
    quick_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);
    quick_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
수행 속도:79
수행 성능 테스트2 자료 개수:100
수행 속도:6

퀵 정렬의 결과를 보면 자료 개수가 MAX_DATA일 때 MAX_DATA/10일 때보다 130배 정도 느린 것을 알 수 있습니다. 앞에서 알고리즘 성능을 분석할 때 O(nlogn)과 비슷하죠. 이제까지 구현했던 순차, 거품, 선택, 삽입 정렬 알고리즘보다 수행 성능이 뛰어난 것을 알 수 있습니다.