Computer >> 컴퓨터 >  >> 프로그래밍 >> C 프로그래밍

C/C++ 병합 정렬(Merge Sort)로 배열의 인버전(Inversion) 개수 구하기

배열의 인버전(Inversion)이란?

배열의 인버전(inversion, 역전)이란 해당 배열을 정렬된 상태로 만들기 위해 필요한 교환 횟수를 의미합니다. 이미 정렬된 배열의 인버전 개수는 0이며, 반대로 역순으로 정렬된 배열에서는 인버전 개수가 최대가 됩니다.

이 문제는 두 개의 반복문을 중첩해 모든 쌍을 비교하는 단순한 방식(O(n²))보다 병합 정렬(Merge Sort)을 활용하면 O(n log n)의 시간 복잡도로 해결할 수 있습니다. 이는 대표적인 분할 정복(Divide and Conquer) 알고리즘 방식입니다.

입력 및 출력 예시

입력

숫자 시퀀스: (1, 5, 6, 4, 20)

출력

주어진 숫자들을 오름차순으로 정렬하는 데 필요한 인버전의 개수를 출력합니다.

인버전의 개수: 2
첫 번째 교환 후: (1, 5, 4, 6, 20)
두 번째 교환 후: (1, 4, 5, 6, 20)

알고리즘

merge(array, tempArray, left, mid, right)

입력 - 병합할 배열과 왼쪽(left), 중간(mid), 오른쪽(right) 인덱스
출력 - 정렬된 순서로 병합된 배열과 이 과정에서 발생한 인버전 개수

시작
   i := left, j := mid, k := left
   count := 0
   i <= mid - 1 이고 j <= right 인 동안 반복
      만약 array[i] <= array[j] 라면
         tempArray[k] := array[i]
         i와 k를 1씩 증가
      아니면
         tempArray[k] := array[j]
         j와 k를 1씩 증가
         count := count + (mid - i)
   반복 종료
   왼쪽 부분에 남은 요소가 있는 동안
      tempArray[k] := array[i]
      i와 k를 1씩 증가
   반복 종료
   오른쪽 부분에 남은 요소가 있는 동안
      tempArray[k] := array[j]
      j와 k를 1씩 증가
   반복 종료
   count 반환
끝

mergeSort(array, tempArray, left, right)

입력 - 주어진 배열과 임시 배열, 배열의 왼쪽·오른쪽 인덱스
출력 - 정렬 과정에서 발생한 총 인버전 개수

시작
   count := 0
   만약 right > left 라면
      mid := (right + left) / 2
      count := mergeSort(array, tempArray, left, mid)
      count := count + mergeSort(array, tempArray, mid+1, right)
      count := count + merge(array, tempArray, left, mid+1, right)
   count 반환
끝

C/C++ 예제 코드

#include <iostream>
using namespace std;

int merge(int arr[], int temp[], int left, int mid, int right) {
    int i, j, k;
    int count = 0;
    i = left;   // 첫 번째(왼쪽) 배열의 위치를 가리키는 인덱스
    j = mid;    // 두 번째(오른쪽) 배열의 위치를 가리키는 인덱스
    k = left;   // 병합된 배열의 위치를 가리키는 인덱스
    while ((i <= mid - 1) && (j <= right)) {
        if (arr[i] <= arr[j]) { // 왼쪽 요소가 더 작거나 같으면 그대로 복사
            temp[k++] = arr[i++];
        } else { // 오른쪽 요소가 먼저 배치되므로 역전 발생
            temp[k++] = arr[j++];
            count += (mid - i); // 왼쪽에 남은 모든 요소와 역전 관계 성립
        }
    }
    while (i <= mid - 1)  // 왼쪽 리스트에 남은 요소 처리
        temp[k++] = arr[i++];
    while (j <= right)    // 오른쪽 리스트에 남은 요소 처리
        temp[k++] = arr[j++];
    for (i = left; i <= right; i++)
        arr[i] = temp[i]; // 임시 배열의 내용을 원본 배열로 복사
    return count;
}

int mergeSort(int arr[], int temp[], int left, int right) {
    int mid, count = 0;
    if (right > left) {
        mid = (right + left) / 2;   // 배열의 중간 인덱스 계산
        count = mergeSort(arr, temp, left, mid);   // 왼쪽 하위 배열 정렬
        count += mergeSort(arr, temp, mid + 1, right);   // 오른쪽 하위 배열 정렬
        count += merge(arr, temp, left, mid + 1, right);   // 두 하위 배열 병합
    }
    return count;
}

int arrInversion(int arr[], int n) {
    int temp[n];
    return mergeSort(arr, temp, 0, n - 1);
}

int main() {
    int arr[] = {1, 5, 6, 4, 20};
    int n = 5;
    cout << "Number of inversions are " << arrInversion(arr, n);
}

실행 결과

Number of inversions are 2

동작 원리 정리

핵심은 병합(merge) 단계에 있습니다. 왼쪽 부분 배열의 요소 arr[i]가 오른쪽 부분 배열의 요소 arr[j]보다 크다면, 왼쪽 부분 배열에서 i부터 mid - 1까지의 모든 요소가 arr[j]와 역전 관계에 있습니다. 따라서 한 번의 비교만으로 여러 개의 인버전을 동시에 셀 수 있으며, 덕분에 전체 시간 복잡도가 O(n log n)으로 줄어듭니다.