게시글

5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭
강좌4 - Algorithm - 8년 전 등록

합병(머지, 병합) 정렬

안녕하세요. 이번 시간에는 합병 정렬(머지 정렬 또는 병합 정렬)에 대해 알아보겠습니다. 삽입 정렬보다는 더 복잡하지만, 성능은 훨씬 좋아 자주 쓰이는 정렬 방법 중 하나입니다. 

합병 정렬은 O(NlogN)이기 때문에 성능이 준수합니다. 다만 30개 이하의 숫자를 정렬할 때는 삽입 정렬과 별 차이도 없고, 정렬하는데 추가적인 메모리가 필요하다는 단점이 있습니다. 보통은 재귀 함수를 사용해서 만듭니다. 삽입 정렬보다 훨씬 더 어렵고 헷갈리니 차분히 따라오세요.

합병 정렬분할 정복 알고리즘에 속합니다. 유명한 수학자 폰 노이만이 개발했죠. 분할 정복이란 어떤 문제를 그대로 해결할 수 없을 때, 작은 문제로 분할해서 푸는 방법을 말합니다. 합병 정렬은 배열을 두 개로 나누고, 나눈 것을 다시 두 개로 계속 나눠 정렬합니다. 어떻게 나누기만 하는데 정렬이 되냐고요? 당연히 정렬이 안 되죠. 중간에 어떤 과정을 넣어주어야 합니다. 같이 볼까요?

일단 정렬이 안 된 배열이 있습니다. 그리고 결과를 저장할 빈 배열이 있습니다.
[2,4,5,7,1,3,6,8], [결과배열]
이 배열을 반으로 쪼갭니다.
[2,4,5,7],[1,3,6,8],[]
이제 두 배열의 제일 앞 수를 비교해 작은 숫자를 결과 배열에 넣어줍니다.
[2,4,5,7],[3,6,8],[1]
[4,5,7],[3,6,8],[1,2]
[4,5,7],[6,8],[1,2,3]
[5,7],[6,8],[1,2,3,4]
... 계속 반복합니다. 결국 다음과 같이 됩니다.
[], [], [1,2,3,4,5,6,7,8]
정렬이 완료되었습니다.

뭔가 당황스럽죠? 너무 쉽게 끝난 것 같습니다. 아직 재귀는 사용하지도 않았습니다. 그런데 눈치채셨나요? 처음에 반으로 쪼갤 때, [2,4,5,7], [1,3,6,8]은 이미 정렬되어 있습니다. 항상 이렇게 정렬되어 있지는 않겠죠? 예를 들면 [5,2,4,7,6,1,3,8]를 반으로 쪼개면 [5,2,4,7], [6,1,3,8]이 되는데 이렇게 되면 두 배열의 제일 앞 수끼리 비교해서 넣어도 정렬이 안 되니까요. 위의 것을 앞에서 한 방식으로 정렬한 결과는 [5,2,4,6,1,3,7,8]입니다. 망했죠?

바로 여기서 재귀분할 정복이 사용됩니다. 이런 경우는 한 번에 해결할 수 없으니까 작은 문제로 쪼개서 생각합니다. 작은 문제로 쪼개는 것은 재귀를 사용하고요. 즉 아까 [5,2,4,7]과 [6,1,3,8]에 다시 합병 정렬을 실행해주는 겁니다. [5,2,4,7]은 [5,2]와 [4,7]로, [6,1,3,8]은 [6,1]과 [3,8]로 나누어 정렬을 실행합니다.

여기에서도 문제가 한 번 더 발생합니다. [5,2]랑 [6,1]은 아직 정렬이 안 된 상태입니다. [5,2]도 [5]와 [2]로 다시 나누어 실행해줍니다. [5],[2]는 정렬하면 [2,5]가 되고, [4,7]은 그대로, [6],[1]은 [1,6]으로, [3,8]은 그대로 정렬됩니다. 이제 최소 단위로는 다 정렬되었으니 [2,5], [4,7]을 정렬하고, [1,6],[3,8]을 정렬하면 됩니다.

결국에는 [5,2,4,7]과 [6,1,3,8]이 각각 [2,4,5,7], [1,3,6,8]로 바뀌겠죠? 이걸 아까처럼 최종적으로 정렬하면 됩니다. 이해가 되셨나요? 이렇게 반복적으로 쪼개서 마지막에 합치는 게 합병 정렬입니다. 배열 원소의 개수가 홀수여도 상관없습니다. 대충 반으로 쪼개서 앞 두 수끼리 비교하면 되거든요. 코드로 볼까요? 크게 재귀를 하는 부분(mergeSort)과, 앞의 두 수끼리 비교(merge)하는 부분입니다.

var mergeSort = function(array) {
  if (array.length < 2) return array; // 원소가 하나일 때는 그대로 내보냅니다.
  var pivot = Math.floor(array.length / 2); // 대략 반으로 쪼개는 코드
  var left = array.slice(0, pivot); // 쪼갠 왼쪽
  var right = array.slice(pivot, array.length); // 쪼갠 오른쪽
  return merge(mergeSort(left), mergeSort(right)); // 재귀적으로 쪼개고 합칩니다.
}
function merge(left, right) {
  var result = [];
  while (left.length && right.length) {
    if (left[0] <= right[0]) { // 두 배열의 첫 원소를 비교하여
      result.push(left.shift()); // 더 작은 수를 결과에 넣어줍니다.
    } else {
      result.push(right.shift()); // 오른쪽도 마찬가지
    }
  }
  while (left.length) result.push(left.shift()); // 어느 한 배열이 더 많이 남았다면 나머지를 다 넣어줍니다.
  while (right.length) result.push(right.shift()); // 오른쪽도 마찬가지
  return result;
};

mergeSort([5,2,4,7,6,1,3,8]); // [1, 2, 3, 4, 5, 6, 7, 8]

숫자가 8개밖에 안 되는데 살짝 느린 감이 있네요. 제 컴퓨터가 낡아서 그렇습니다... 더 많은 숫자를 넣어 정렬해보세요! 컴퓨터가 어디까지 버티나 테스트해보자고요. 참고로 자바스크립트의 배열은 정말 편리합니다. 저 shift 메소드가 얼마나 편리한 건지 다른 언어(특히 C라든가...)도 사용하신다면 뼈저리게 느끼실 겁니다.

mergeSort의 단점은 array 외에도 result라는 추가적인 저장 공간이 필요하다는 겁니다. array의 크기가 커질수록 result의 크기도 커지겠죠. 메모리가 싸진 요즘 같은 경우는 큰 문제가 안 되지만, 수백만 개의 데이터를 처리하는 경우는 문제가 될 수도 있습니다. 앞으로 정렬 방법을 보실 때는 O()가 뭔지도 중요하지만 return 값이 array인지 새로운 배열 result인지도 중요합니다. 새로운 배열을 return한다는 것은 그만큼 메모리를 더 잡아먹는다는 뜻이니까요.

참고로 일부 브라우저에서는 배열.sort()를 할 때 합병 정렬을 사용한다네요. 이번 시간은 많이 어려웠으니까 다음 시간에는 쉬운 버블 정렬에 대해 알아보겠습니다!

조회수:
0
목록
투표로 게시글에 관해 피드백을 해주시면 게시글 수정 시 반영됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright 2016- . 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.
5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭

댓글

9개의 댓글이 있습니다.
3년 전
저도 return result;가 되고 나서 다시 mergeSort의 merge로 가지는 부분이 이해가 잘 안 가는데요.

처음 mergeSort에서 길이 8짜리 배열을 쪼갠 뒤 각각 left, right을 mergeSort에 다시
보내면
콜스택에 새로운 mergeSort가 각각 쌓여서 각 mergeSort는 이전 배열들(더 잘게 쪼개지기 전의)을
갖고있기 때문에
하나의 콜스택이 끝나고 나면 이전 콜스택이 실행되어지는 건가요..? 너무어렵네요..
4년 전
덕분에 확실히 이해 되었습니다. 감사합니다!
4년 전
하나의 값이 while문을 돌고 난 후 또 다른 값을 받아올 수 있는건 재귀함수로 인해 해당 값들이 left, right 안에 존재하기 때문인건가요?
4년 전
네네. 피보나치 재귀함수 생각해보시면 됩니다~. 이전에 실행된 값들이 계속 메모리에 남아있습니다.
4년 전
6번째줄이 헷갈리는데요 left와 right에는 하나의 값만 들어있는데 어떻게 전체의 값이 결과로 나오는지 모르겠어요ㅜ 혹시 제가 잘못이해한 건가요?
4년 전
재귀 함수의 특성을 먼저 이해하셔야 할 것 같습니다. left와 right는 배열로 쪼갠 단계에서는 하나의 값만 들어있을 수 있지만 merge하면서 여러 개의 값으로 다시 합쳐집니다.
5년 전
안녕하세요 혹시 `left.length && right.length` 이 부분의 의미가 left 및 right의 값이 empty가 아닐 때가 맞을까요?
5년 전
넵. 0이 아닐 때입니다.
5년 전
제로초님 안녕하세요 유튜브랑 블로그 항상 잘보구 있습니다(거의 유튜브쪽을주로봅니당)!!
현재 자바스크립트 무료강의 보구있구 알고리즘도 공부하고싶어서 블로그좀 보구 있는데
벌써 부터 어렵네요ㅠ.ㅠ(6번째줄에서 재귀되면서 코드를 공책에 쓰면서 봐도 헷갈리네요)
향후에 알고리즘을 유튜브 무료or유료 강의계획이 있는지 궁금합니다 항상 알고리즘은 공부 해보고싶은데 먼가 혼자서 블로그보면서 해봐도 조금어렵네요ㅠㅠ;;
긴글 읽어주셔서 감사하구 앞으로도 좋은책이랑 강의 많이 해주시면 감사하겠습니다!!!
5년 전
정말감사합니다
7년 전
쉽게 이해가 가네요 감사합니다^^
7년 전
감사합니다. 사실 알고리즘 공부도 처음이고 자바스크립트도 처음이라 이해하기 어려웠는데 설명해주신 내용을 유심히 보고 제가 짤 수 있는 자바 언어로 짰습니다!