3 minute read

모두의 알고리즘 - part 3 - 2 : 정렬알고리즘 - 병합정렬

병합정렬

전체배열(0<N)에서 일정 숫자를 그룹으로 묶는다..
=> 각 그룹끼리 정렬을 실시한다
=> 각 그룹끼리 제일 첫번째 인수 끼리 비교를 진행한다.
=> 작은 값을 뽑아서 큰 그룹으로 포함시킴 만듦
=> 이 과정을 반복한다.

  • 삽입정렬의 알고리즘 골격 ```java //내부적으로 재귀호출을 사용하며 정렬을 수행한다 mergeSort(A[1 … n]){

      //[알고리즘 종료 조건]
      if(n <= 1>){
          return// 정렬 필요 없음
      }
    
      mid = n/2 // 전체 배열을 두그룹으로 나눔
    
      g1[] = mergeSort(A[1 ... n/2]) // 각 그룹 끼라 병합 정렬 -> 최종 2가 남을 때까지 
      g2[] = mergeSort(A[n/2 ... n])
    
      //두 그룹을 하나로 병합
      result[]
    
      i = 0
      j = 0
      // 둘다 있으면 각각 비교
      while (g1.length > 0 && g2.length > 0){//핵심기능
          if(g1[i] > g2[i]){
              result.add(g1.pop[0])//핵심기능
          }
          else{
              result.add(g2.pop[0])//핵심기능
          }
      }
      //남은 것들은 
      while (g1.length > 0){//핵심기능
          result.add(g1.pop[0])//핵심기능
      }
        
      while (g2.length > 0){//핵심기능
          result.add(g2.pop[0])//핵심기능
      }
    
      return result
    

    }

- Java pop 기능 없이 구현 : 해당 알고리즘 골격으로 java를 구현하려 많은 에러가 발생함
    - remove(pop) 기능에 따른 배열의 size의 순차적 감소가 이 알고리즘의 핵심 골격임.
    - **[문제점 1]** arrayList에 pop에 대응하는 remove를 반복문에 사용하니 java.util.**ConcurrentModificationException** 발생
    - **[문제점 2]** iterator를 사용하면 비슷한 기능을 사용할 수 있지만 next를 한번 사용하면 다시 뒤로 뺄수 가 없어 반복조건이 발생하지 않음, 최종적으로 이진트리에서 값 비교가 이루어짐.
    - 위에 해당하는 문제점을 해결하는 방법은 그냥 remove 기능을 안쓰고 인덱스에 맞춰 result에 add 시킴.
    ```java
        MergeSort.java 참조.
    ```

- 일반적인 병합 정렬 알고리즘 골격 : return 없이 배열 안에서 값이 변경되어 처리됨
``` python

    def merge_sort_2(a):
        n = len(a)

        if n<=1:
            return

        mid = n // 2

        g1 = a[:mid]
        g2 = a[mid:]

        merge_sort_2(g1)
        merge_sort_2(g2)

        i = 0
        j = 0
        a_idx = 0
        while(i < len(g1) and j < len(g2)):
            if g1[i] < g2[j]:
                a[a_idx] = g1[i]
                i++
                a_idx++
            else:
                a[a_idx] = g2[j]
                j++
                a_idx++

        while(i < len(g1)):
            a[a_idx] = g1[i]
            i++
            a_idx++

        while(j < len(g2)):
            a[a_idx] = g2[j]
            j++
            a_idx++
  • Java로 구현

https://github.com/chrisna2/AlgorithmWithJava/blob/master/src/part3/merge/MergeSort.java 참조


    private static List<Integer> mergeSort(List<Integer> arr) {

        //재귀함수 종료 조건
        if(arr.size() <= 1){
            return arr;
        }

        int mid = arr.size()/2;
        int end = arr.size();

        List<Integer> g1 = mergeSort(arr.subList(0, mid));
        List<Integer> g2 = mergeSort(arr.subList(mid, end));

        List<Integer> result = new ArrayList<>();
        
        /*
            [debug] 이런 식으로 하게 되면 java.util.ConcurrentModificationException 이 발생한다. 
            자바는 파이선,자바스크립트와 다르게 앞에서 부터 인덱스가 변경되면 엘리먼트의 인덱스가 실시간으로 변함
            Length가 변경되면서 해당 인덱스의 값이 null 이 되기 때문에 발생
            
            즉 자바는 for, while 같이 루핑이 도는 와중에 루핑의 조건이 되는 인덱스가 실시간으로 변경이 되면 해당에러 발생
            파이썬, JS가 이런면에서는 편리함 

            while(g1.size() > 0 && g2.size() > 0){
                
                if(g1.get(0) < g2.get(0)){
                    result.add(g1.remove(0));
                }
                else{
                    result.add(g2.remove(0));
                }
            } 
            
            while(g1.size() > 0){
                result.add(g1.remove(0));
            }
            
            while(g2.size() > 0){
                result.add(g2.remove(0));
            }

        */
        
        /* 
            해결법 
            그냥 remove를 안쓰고 인덱스 값으로 셋팅한다. -> 제일 간단한 방법 
        */
        int i = 0;
        int j = 0;
        while(g1.size() > i && g2.size() > j){
            if(g1.get(i) < g2.get(j)){
                result.add(g1.get(i));
                i++;
            }
            else{
                result.add(g2.get(j));
                j++;
            }
        } 
        
        while(g1.size() > i){
            result.add(g1.get(i));
            i++;
        }
        
        while(g2.size() > j){
            result.add(g2.get(j));
            j++;
        }
            
        return result;

    }

[문제점 1] sublist 메서드를 배열로 잘라서 각각의 변수로 직접 지정할 경우, 상위 배열의 값이 바뀔때 잘라서 변수로 지정된 배열의 값도 같이 바뀐다.

병합 정렬의 효용성

  • 이런 방식의 큰 문제를 작은 문제로 나눠서(분할) 푸는(정복) 방식을 “분할 정복 방식”이라고 한다.
  • 분할 정복 방식의 시간 복잡도는 O(n $\log{n}$) 으로 선택정렬 O($n^2$), 삽입정렬 최악의 경우 O($n^2$) 보다 낮은 복잡도를 가진다.
  • 따라서 정렬해야 될 데이터가 많을 수록 병합정렬리 더 빠른 결과를 냄

part3 공부하면서 배운 소소한 java 팁

1. 배열의 pop, push의 java기능 대응

    |JS|            |JAVA|
    Array.push    -> ArrayList.add(Object o); // Append the list
    Array.pop     -> ArrayList.remove(int index); // Remove list[index]
    Array.shift   -> ArrayList.remove(0); // Remove first element
    Array.unshift -> ArrayList.add(int index, Object o); // Prepend the list

[문제점 1] 이런 식으로 하게 되면 java.util.ConcurrentModificationException 이 발생한다. 자바는 파이선,자바스크립트와 다르게 앞에서 부터 인덱스가 변경되면 엘리먼트의 인덱스가 실시간으로 변하지 않음. Length가 변경되면서 해당 인덱스의 값이 null 이 되어 해당 에러가 발생함. 반복문 상에서 remove를 사용할 수 없는 이유. add는 상관없음.

즉 자바는 for, while 같이 루핑이 도는 와중에 루핑의 조건에 remove가 사용되어 인덱스가 실시간으로 변경이 되면 해당에러 발생. 파이썬, JS가 이런면에서는 편리함.

2. ArrayList 분리 기능 파이썬 <-> JAVA

  • 파이썬
      a = [];
      n = len(a);    
      mid = n//2 
    
      g1 = mergeSort(A[:mid]) 
      g2 = mergeSort(A[mid:])
    
  • JAVA
      List<Integer> arr = new ArrayList<Integer> arr;
      int mid = arr.size()/2;
      int end = arr.size();
    
      List<Integer> g1 = mergeSort(arr.subList(0, mid));
      List<Integer> g2 = mergeSort(arr.subList(mid, end));
    

    [주의!] sublist 메서드를 배열로 잘라서 각각의 변수로 직접 지정할 경우, 상위 배열의 값이 바뀔때 잘라서 변수로 지정된 배열의 값도 같이 바뀐다. ```java //배열을 자르고 변수를 다르게 받는다고 해서 메모리까지 분리된게 아님. //아래와 같이 사용을 하면 arr을 변경할 경우- g1. g2 까지 영향을 받음

    List g1 = arr.subList(0, mid); List g2 = arr.subList(mid, end);

    //아래와 같이 사용해야 메모리 까지 완전히 분리가 됨 List g1 = new ArrayList<>(); List g2 = new ArrayList<>(); g1.addAll(arr.subList(0, mid)); g2.addAll(arr.subList(mid, end));

```

Comments