반응형

 

이번에는 백준 24060번 문제를 통해 병합 정렬 개념을 다시 다져보자.
이번 문제도 저번 개념 정리와 같이 재귀 방식으로 계속 나누고 
병합하며 정렬한다.

 

 

[ 알고리즘 ] 병합 정렬 - JAVA

백준 24060번 문제를 풀기 전에 병합 정렬 알고리즘의 로직에 대해아주 자세히 포스팅 해보려 한다. 사실 이미지로 본다면 아주 쉬운데 이걸 구현하고자 하면 머리가 아플 수도 있다. 때문에 이

p-coding.tistory.com

 

🔍 문제 핵심

  • 병합 정렬은 정렬 알고리즘 중 하나로, 분할 정복(Divide and Conquer) 방법을 사용한다.
  • 주어진 배열을 계속해서 절반으로 나누고, 더 이상 나눌 수 없을 때까지 나눈 뒤, 정렬된 상태로 병합한다.
  • 문제에서 예시로 주어진 병합 정렬 과정을 잘 이해하고, 이를 직접 구현해보는 것이 핵심이다.

📌 병합 정렬이란?

1. 분할 (Divide)

  • 배열을 절반으로 나눈다.
  • 이 과정을 배열의 크기가 1이 될 때까지 반복한다.
  • 각 하위 배열은 재귀적으로 처리된다.

2. 정복 (Conquer)

  • 크기가 1인 배열끼리 **두 개씩 병합(Merge)**하며 정렬해나간다.
  • 병합 과정에서 두 배열의 원소를 비교하여 작은 값을 먼저 넣는 방식으로 정렬이 이루어진다.

3. 병합 (Combine)

  • 정렬된 두 배열을 하나로 합치며 정렬을 유지한다.

 

 

 

재귀 방식으로 정렬하는 복잡한 구조로 공부하기 좋은 문제이다.
이해도를 높이기 위해 많은 관련 문제를 푸는 것을 추천한다.

전체 코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {

    private static int count = 0;
    private static int number = -1;
    private static int K;

    private static void mergeSort(int[] arr, int left, int right) {
        if(left < right) {
            int mid = (left + right) / 2;

            mergeSort(arr, left, mid);
            mergeSort(arr, mid + 1, right);
            merge(arr, left, mid, right);
        }
    }

    private static void merge(int[] arr, int left, int mid, int right) {

        int[] temp = new int[right - left + 1];
        int i = left, j = mid + 1;
        int z = 0;

        while(i <= mid && j <= right) {
            if(arr[i] <= arr[j]) {
                temp[z++] = arr[i++];
            }
            else {
                temp[z++] = arr[j++];
            }

        }

        while(i <= mid) {
            temp[z++] = arr[i++];
        }

        while(j <= right) {
            temp[z++] = arr[j++];
        }


        for (int l = 0; l < temp.length; l++) {
            arr[left + l] = temp[l];
            count++;
            if(count == K) {
                number = temp[l];
            }
        }


    }

    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer stringTokenizer = new StringTokenizer(bufferedReader.readLine());

        int N = Integer.parseInt(stringTokenizer.nextToken());
        K = Integer.parseInt(stringTokenizer.nextToken());

        int[] arr = new int[N];

        stringTokenizer = new StringTokenizer(bufferedReader.readLine());
        for (int i = 0; i < N; i++) {
            arr[i] = Integer.parseInt(stringTokenizer.nextToken());
        }

        mergeSort(arr, 0, N - 1);

        System.out.println(number);

    }
}
반응형
반응형
백준 24060번 문제를 풀기 전에 병합 정렬 알고리즘의 로직에 대해
아주 자세히 포스팅 해보려 한다.
사실 이미지로 본다면 아주 쉬운데 이걸 구현하고자 하면 머리가 아플 수도 있다. 
때문에 이해하기 언제보더라도 이해하기 쉽게,
이번 포스팅은 한줄 한줄 뜯어보도록 하겠다.

합병 정렬 ( Merge Sort )

합병 정렬 or 병합 정렬이라고 하며, 비교 기반이고, 분할 정복 알고리즘 중 하나이다.

 존 폰 노이만이 1945년에 개발했다.

 

최악, 평균, 최선에서 일정한 시간 복잡도 O(n log n)를 가지며,

이는 배열을 나누는데 O(log n)

합병하는데 O(n)이 걸리기 때문이다.

 

순서는 다음과 같다.

 

1. 분할

정렬되지 않은 리스트를 절반으로 잘라 비슷한 크기의 두 부분으로 나눈다.

 

2. 정복

각 부분 리스트를 재귀과정으로 정렬한다.

 

3. 결합

두 부분을 하나의 정렬된 리스트로 합병한다.

 

4. 복사

임시 배열에 저장된 결과를 원래 배열에 복사

 


 

다음과 같은 예시로 알아보자

8개의 정수를 가진 배열 Array가 존재한다.

int[] arr = {38, 27,33, 1, 43, 3, 9, 10};

 

다음을 그림과 같이 표현할 수 있다.

 

사실 이 그림은 합병 정렬이라면 지겹게 보았을 거다.

근데 이 그림만 보고서는 재귀적 요소 때문에 코드가 잘 이해되지 않을 수 있다.

 

분할

private static void mergeSort(int[] arr, int left, int right) {
    if(left < right) {
        int mid = (left + right) / 2;

        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);
        merge(arr, left, mid, right);
    }
}

 

결합

private static void merge(int[] arr, int left, int mid, int right) {

    int[] temp = new int[right - left + 1];
    int i = left, j = mid + 1;
    int k = 0;

    while(i <= mid && j <= right) {
        if(arr[i] <= arr[j]) {
            temp[k++] = arr[i++];
        }
        else {
            temp[k++] = arr[j++];
        }
    }

    while(i <= mid) {
        temp[k++] = arr[i++];
    }

    while(j <= right) {
        temp[k++] = arr[j++];
    }


    for (int l = 0; l < temp.length; l++) {
        arr[left + l] = temp[l];
    }


}

 

MergeSort 메서드 - 1 [ mergeSort(arr, 0, 7) ]

  •  다음과 같이 분할되어 재귀호출 한다.

MergeSort 메서드 - 2 [ mergeSort(arr, 0, 3) ]

  •  다음과 같이 분할되어 재귀호출 한다.

 

MergeSort 메서드 - 3 [ mergeSort(arr, 0, 1) ]

  • 이번에는 left == mid 와 mid + 1 == right 이기  때문에 if문에서 막혀 실행되지 않는다.
  • 바로 다음 merge() 메서드가 실행된다.

Merge 메서드 - 4 [ merge(arr, 0, 0, 1) ]

  • 먼저 병합할 개수만큼 temp 배열을 생성한다.
  • 이후 다음 코드와 같이 각각 시작할 값을 설정한다
    • 왼쪽 분할 리스트에서 시작값 --> left ex) 0
    • 오른쪽 분할 리스트에서 시작값 -> mid + 1 ex) 1

  • 이후 다음 첫 번째 코드로 인해 temp에 위 그림과 같이 저장된다.
  • 그 다음 두 번째 코드로 기존 배열에 정렬된 값이 저장된다.
    • 기존의 값들 만큼만 꺼내고 그 값들로만 정렬하여 다시 재배치 하기 때문에 그대로 넣어주기만 하면 된다.
while(i <= mid && j <= right) {
    if(arr[i] <= arr[j]) {
        temp[k++] = arr[i++];
    }
    else {
        temp[k++] = arr[j++];
    }
}
for (int l = 0; l < temp.length; l++) {
    arr[left + l] = temp[l];
}

MergeSort 메서드 - 5 [ mergeSort(arr, 2, 3) ] & Merge 메서드 - 6 [ merge(arr, 2, 2, 3) ]

  • 2번 째 스텝의 분할된 오른쪽 리스트를 처리할 차례이다.
  • 4번 째와 동일한 방법으로 실행된다. ( 간략하게 코드는 제외 후 그림만 )

Merge 메서드 - 7 [ merge(arr, 0, 1, 3) ]

  •  2번 째 스텝에서 재귀 호출을 모두 마치고 이 코드로 돌아왔다.
  • 그렇다면 이제 합병 정렬할 차례이다.

  • 먼저 초기 값들은 다음과 같이 구성된다.

  • 이후 다음과 같은 코드를 통해 진행된다.
while(i <= mid && j <= right) {
    if(arr[i] <= arr[j]) {
        temp[k++] = arr[i++];
    }
    else {
        temp[k++] = arr[j++];
    }
}

 

1회전 - arr[i] = 27 > arr[j] = 1

2회전 - arr[ i ] = 27 <= arr[ j ] = 33

3회전 - arr[ i ] = 38 > arr[ j ] = 33

  • 이 이후는 j = 4로 right = 3 보다 크기 때문에 while문을 빠져나온다.
  • 그렇다면 잔여값은 어떻게 될까

 

 

while(i <= mid) {
    temp[k++] = arr[i++];
}

while(j <= right) {
    temp[k++] = arr[j++];
}

 

  • 위 코드를 통해 기존의while(i <= mid && j <= right) { 코드 형태를 유지한 채 i or j 중 최대에 도달하지 못했다면 남은 자리를 잔여 값으로 채워준다.
  • 다음 잔여값을 넣어야 할 위치는? k가 가지고 있다.

  • 이후 이전과 같이 temp값을 그대로 넣어준다.
for (int l = 0; l < temp.length; l++) {
    arr[left + l] = temp[l];
}

이후

이후에는 분할된 남은 부분 즉, MergeSort 메서드 - 1 [ mergeSort(arr, 0, 7) ] 부분에서 이루어진 오른쪽 리스트들을 분할 정복하면 된다. 이는 그림과 같이 한번 직접 해보시길 바란다.

 

정렬 전 : [38, 27, 33, 1, 43, 3, 9, 10]
정렬 후 : [1, 3, 9, 10, 27, 33, 38, 43]

 

 

합병 정렬은 임시 메모리를 생성해야 하기 때문에
큰 배열에서는 메모리 사용량이 크다. 하지만 안정적인 정렬로 고려해서 사용하는게 좋다.

이번 정렬은 간단하면서도 복잡한 알고리즘으로
기본적인 분할 정복 알고리즘의 대표적인 예라고 생각한다.
때문에 꼭 이 알고리즘을 정복하고 넘어가는 것을 추천한다.

전체 코드

import java.util.Arrays;

//TIP 코드를 <b>실행</b>하려면 <shortcut actionId="Run"/>을(를) 누르거나
// 에디터 여백에 있는 <icon src="AllIcons.Actions.Execute"/> 아이콘을 클릭하세요.
public class Main {

    private static void mergeSort(int[] arr, int left, int right) {
        if(left < right) {
            int mid = (left + right) / 2;

            mergeSort(arr, left, mid);
            mergeSort(arr, mid + 1, right);
            merge(arr, left, mid, right);
        }
    }

    private static void merge(int[] arr, int left, int mid, int right) {

        int[] temp = new int[right - left + 1];
        int i = left, j = mid + 1;
        int k = 0;

        while(i <= mid && j <= right) {
            if(arr[i] <= arr[j]) {
                temp[k++] = arr[i++];
            }
            else {
                temp[k++] = arr[j++];
            }
        }

        while(i <= mid) {
            temp[k++] = arr[i++];
        }

        while(j <= right) {
            temp[k++] = arr[j++];
        }


        for (int l = 0; l < temp.length; l++) {
            arr[left + l] = temp[l];
        }


    }

    public static void main(String[] args) {
        int[] arr = {38, 27, 33, 1, 43, 3, 9, 10};
        System.out.println("정렬 전 : " + Arrays.toString(arr));
        mergeSort(arr, 0, arr.length - 1);

        System.out.println("정렬 후 : " + Arrays.toString(arr));
    }
}
반응형
반응형
요즘 다시 알고리즘 공부를 시작하려고 하여
백준에서 문제를 풀어나가며 알고리즘을 따로 정리, 기술하는 방향으로 계획하고 있다.
그 중 정렬 알고리즘을 먼저 살펴보려고 하며,
오늘은 기본 중 하나인 선택 정렬를 알아보고자 한다.

선택 정렬 ( Selection Sort )

 

선택 정렬은 간단하지만 비효율적인 정렬 알고리즘 중 하나로, 배열에서 가장 큰 or 가장 작은 값을 찾아 현재 위치에 배치하는 과정을 반복하여 정렬된다.

 

순서는 다음과 같다.

 

1. 정렬되지 않은 부분에서 최솟값 or 최댓값 찾기

 

2. 현재 위치와 최솟 값 or 최대 값 교환

 

3. 다음 위치로 이동하며 반복


 

다음 그림과 같이 예시로 알아보자

5개의 정수 값을 가진 배열 Array가 존재한다.

이를 오름차순으로 왼쪽부터 최솟값을 찾아 정렬하고자 한다.

 


 

기본 형태

4 5 3 1 2

 


1회차

 

첫번 째 위치인 4를 기준

4 5 3 1 2

 

 

차례로 5, 3, 1, 2 와 비교

4 5 3 1 2

 

 

최솟값을 찾아 위치를 교환

4 5 3 1 2

 

 

1회차 완료

1 5 3 4 2

 


 

2회차

 

두번 째 위치인 5를 기준

1 5 3 4 2

 

 

차례로 3, 4, 2 와 비교

1 5 3 4 2

 

 

최솟값을 찾아 위치를 교환

1 5 3 4 2

 

 

2회차 완료

1 2 3 4 5

 

 

2회차만에 정렬되어 정렬된 배열은 다음과 같다.

1 2 3 4 5

전체 코드

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int [] arr = {6,4,2,1,3,5};
        selectionSort(arr);
        Arrays.stream(arr).forEach(System.out::println);
    }

    private static void selectionSort(int[] arr) {
        if(arr == null || arr.length < 2) {
            return;
        }

        int minIndex;
        for(int i = 0; i < arr.length - 1; i++) {
            minIndex = i;
            for(int j = i + 1; j < arr.length; j++) {
                if(arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }

            if(minIndex != i) {
                int temp = arr[minIndex];
                arr[minIndex] = arr[i];
                arr[i] = temp;
            }
        }
    }
}

 

 

구현은 쉽고 직관적이긴 하나,  선택 정렬은 앞서 말했듯 입력 배열에 관계없이(2회차에 정렬이 되었더라도)
외부 반복문에서 n -1 회, 내부 루프에서 n - 1, n - 2, ..., 1번 비교함으로
시간복잡도는 항상 O(n^2) 으로 비효율적이다. 

 

반응형

+ Recent posts