병합 정렬

병합 정렬

배열 앞부분(영역)과 뒷부분(영역)으로 분할하여 정렬 후 다시 병합(합치기)하는 작업을 반복하며 정렬하는 알고리즘

 

병합 과정?

Do it 자료구조와 함께 배우는 알고리즘 입문

위와같이 배열을 앞, 뒤로 나눠서 각자 정렬한 후에 각각의 분할된 배열을 앞에서부터 서로 비교하며 기존 배열에 병합하게 된다. 이때 비교 대상인 두 배열 중 비교 후 값으로 선택되면 그 값이 있던 배열은 다음 요소로 이동하며 비교해나간다.

//두 배열 병합하기
    static void merge(int[] a,int na,int[] b,int nb, int[] c){
        int pa = 0;
        int pb = 0;
        int pc = 0;

        while(pa < na && pb < nb)
            c[pc++] = a[pa] < b[pb] ? a[pa++] : b[pb++];

        // a , b 배열 중 병합하고 남은 요소가 있을 시
        while(pa < na)
            c[pc++] = a[pa++];

        while (pb < nb)
            c[pc++] = b[pb++];
    }
    
    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);

        //배열이 정렬되었다고 가정하기 위함
        int[] a = {2,4,6,8,11,13};
        int[] b = {1,2,3,4,9,16,21};
        int[] c= new int[a.length+b.length];//병합할 배열

        System.out.println("두 배열을 병합");
        merge(a, a.length , b , b.length , c);

        System.out.println("배열 a와 b를 병합하여 배열 c에 저장!!!");
        System.out.print("a 배열 : ");
        Arrays.stream(a).forEach(i-> System.out.print(i + " "));
        System.out.print("\nb 배열 : ");
        Arrays.stream(b).forEach(i-> System.out.print(i + " "));
        System.out.print("\nc 배열 : ");
        Arrays.stream(c).forEach(i-> System.out.print(i + " "));
    }

여기서는 정렬하는 과정은 생략하고 정렬을 마친 후 병합하는 과정을 코드로 구현했다. 정렬을 마친 두 배열은 각자 정렬기준에 따라 정렬이 되어있기에 각각의 배열 첫번째 요소부터 서로 비교를 해간다.

 

이때 정렬조건에 따라 비교 후 결정되는 요소는 병합 배열에 앞에서부터 차례대로 들어간다. 그리고 요소가 선택된 배열은 병합 배열로 요소가 들어갔기에 다음 요소로 이동하여 비교하고 선택하는 과정을 거친다.

 

최종적으로 비교 선택을 반복하다보면 두 배열 중 더 이상 추가 요소가 없을 때 비교하고 선택하는 과정은 끝이난다. 이때 병합하는 과정에서 두 배열은 동일한 정렬조건에 따라 비교 선택되었기에 한 쪽 배열에 요소가 남았으면 자연히 병합하고자 하는 배열에 순차적으로 들어가게 된다. 이 부분은 결과에 표시한 부분에서 확인할 수 있다.


병합 정렬 구현

위에서 살펴 본 정렬된 두 배열을 병합하는 과정을 응용하여 분할 정복법에 따라 정렬하는 알고리즘을 병합 정렬이라 한다.

전체 배열을 절반씩 나누어 최소로 만들고 나누어진 앞, 뒤 배열을 각각 정렬 후 병합해가며 전체 배열을 정렬

분할 정복법 : 전체 문제를 부분으로 나누어 부분적으로 문제를 해결하여 전체 문제를 해결해 나가는 방법
static int[] buff; //작업용 배열

    static void __mergeSort(int[] a, int left , int right){
        //배열을 분할 시 분할 된 배열크기가 1개가 되기 전까지 분할
        if(left < right){
            int i;
            int center = (left+right) / 2; // 분할을 위한 중앙요소 위치
            int p = 0; // 중앙 기준 앞쪽 요소가 몇개 있는지 체크
            int j = 0; //
            int k = left; //현재 left위치
            
           /*단순 이해를 위한 출력
           System.out.println("=== 현재 배열 ===");
            for(int c = left ; c <= right ; c++){
                System.out.print(a[c]+ " ");
            }
            System.out.println();
            
            if(left < center) System.out.println("=== 부분 앞쪽 ===");*/
            __mergeSort(a, left , center); // 중앙 요소 기준 좌측 배열
            /*단순 이해를 위한 출력
            if(center+1 < right) System.out.println("=== 부분 뒤쪽 ===");*/
            __mergeSort(a,center+1,right); // 중앙 요소 기준 우측 배열

            for(i = left ; i <= center ; i++) // i값은 center + 1이 되면서 빠져나간다.
                buff[p++] = a[i]; // 현재 작업중인 배열 처음부터 중앙까지 저장
             /*
            	*i == right이면 뒤쪽 요소들을 모두 소모
            	*p값은 쉽게 배열의 경우 크기 -1이 마지막 인덱스이다.즉 마지막 요소 위치에 
                  1개 증가한 값이다.(p = buff에 저장된 요소개수)
            	*앞쪽 요소들은 [buff, j]를 통하여 제어 / 뒤쪽은 [a, i]를 통해서 제어
            	*앞쪽 요소들은 buff에 저장이 되어있기에 뒤쪽 요소 중 앞쪽으로 와야할 경우 이동만 
                  해주면 된다.
            	*정렬한다에 대해서 한번 생각해보면 오름차순이거나 내림차순이거나 중요한 것은
                  가장 앞 쪽부터 기준에 맞게 정렬이 된다는 것이다.
                *위에 부분을 생각하며 while에 조건을 확인해보면 최종적으로 앞 뒤 배열 중 한 쪽이
                  먼저 모든 요소가 비교 후 선택되어 더 이상 요소가 없을 시 반복종료된다.
                  이때, 뒤 쪽 배열이 먼저 모두 소모되었다는 것은 앞 쪽으로 모두 정렬되었다는 
                  의미이다. 그렇기에 buff에 남은 앞 쪽 요소들만 순차적으로 남은위치에 정렬해주면 
                  된다. 그러나 앞 쪽이 먼저 소모되었다는 것은 뒤 쪽 배열에 남은 요소들은 정렬기준에
                  따라 앞 쪽 요소들보다 뒤에 위치해야하며, 남은 부분들은 그대로 위치하면 된다.
              */
            while(i <= right && j < p)
                a[k++] =  (buff[j] <= a[i]) ? buff[j++] : a[i++];
            while (j < p) {
                a[k++] = buff[j++];
            }
            /*단순 이해를 위한 출력
            System.out.println("=== 병합 정렬 후 ===");
            for(int c = left ; c <= right ; c++){
                System.out.print(a[c]+ " ");
            }
            System.out.println();
            */
        }
    }

    static void mergeSort(int[] a , int n){
        buff = new int[n];

        __mergeSort(a,0,n-1);
    }
    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);

        System.out.println("병합 정렬");
        System.out.print("요솟수 : ");
        int nx = s.nextInt();
        int[] x = new int[nx];

        for(int i = 0; i < nx ; i++){
            System.out.print("x["+i+"] :");
            x[i] = s.nextInt();
        }
        System.out.println("=== 정렬 전 ===");
        Arrays.stream(x).forEach(i-> System.out.print(i + " "));
        System.out.println();
        mergeSort(x,nx);

        System.out.println("오름차순으로 정렬했습니다.\n");
        System.out.println("=== 정렬 후 ===");
        Arrays.stream(x).forEach(i-> System.out.print(i + " "));
    }

병합 정렬 결과

개인적으로 공부를 하면서 직접 단계별로 어떻게 분할되고 병합되고 정렬되는지 그려보면서 해보았다.

 

그 부분들이 큰 도움이되어 병합 정렬 알고리즘에 대한 자세한 부분들은 위에 코드에 부분별로 확인한 부분과 이해한 부분들을 최대한 적어보았다.

 

자바에서는 Arrays.sort()를 활용하여 메서드 내부적으로 정렬 알고리즘을 구현했기에 이를 활용하면 더욱 쉽게 정렬을 수행할 수 있을 것이다. 기본 자료형 배열은 퀵 정렬, 객체 배열은 병합 정렬을 개선하여 내부적으로 구현했다고 한다. 

'알고리즘&자료구조 > 정렬 알고리즘' 카테고리의 다른 글

힙 정렬  (0) 2022.10.09
도수 정렬  (1) 2022.10.08
퀵 정렬  (1) 2022.10.05
셸 정렬  (1) 2022.10.03
단순 삽입 정렬(straight insertion sort)  (0) 2022.10.01