정렬 알고리즘 (Sorting Algorithm)

Yunsung·2025년 1월 9일
post-thumbnail

정렬(Sorting)은 데이터를 특정 기준에 따라 순서대로 배열하는 과정을 말합니다. 정렬은 많은 알고리즘 문제의 기본이 되는 기술로, 다양한 방식으로 구현됩니다. 이번 글에서는 대표적인 정렬 알고리즘과 각각의 특징을 정리해보겠습니다.


1. 정렬 알고리즘의 분류

1-1. 내부 정렬 vs 외부 정렬

  • 내부 정렬: 데이터를 메모리에 모두 적재한 상태에서 정렬을 수행.
  • 외부 정렬: 데이터가 많아 메모리에 모두 적재할 수 없을 때, 일부만 메모리에 적재하여 정렬.

1-2. 비교 기반 정렬 vs 비교 비기반 정렬

  • 비교 기반 정렬: 요소 간의 크기를 비교하여 정렬.(예: 삽입 정렬, 병합 정렬)
  • 비교 비기반 정렬: 크기 비교 없이 정렬.(예: 기수 정렬, 계수 정렬)

2. 대표적인 정렬 알고리즘

2-1 버블 정렬(Bubble Sort)

  • 원리: 인접한 두 요소를 비교하여 교환. 큰 값이 점점 뒤로 이동. (오른쪽 부터 정렬)
  • 시간 복잡도
    - 최악: O(N2N^2)
    - 최선: O(N) (이미 정렬된 경우)
  • 특징
    - 구현이 간단.
    - 데이터가 거의 정렬된 경우 빠르게 동작.
public class BubbleSort {
    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    // Swap arr[j] and arr[j+1]
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = {64, 34, 25, 12, 22, 11, 90};
        bubbleSort(arr);
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

2-2. 선택 정렬 (Selection Sort)

  • 원리: 배열에서 가장 작은 요소를 찾아 맨 앞부터 교환.
  • 시간 복잡도: O(N2N^2)
  • 특징:
    - 정렬이 완료될 때까지 비교가 계속됨.
    - 입력 데이터의 상태에 상관없이 동일한 성능.
  • 코드
public class SelectionSort {
    public static void selectionSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
        	// 남아있는 배열 중에서 최소값을 찾는다.
            int minIndex = i;
            for (int j = i + 1; j < n; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            // Swap arr[i] and arr[minIndex]
            // 찾은 최소값을 정렬되지 않은 부분의 첫 번째 요소와 교환한다.
            int temp = arr[minIndex];
            arr[minIndex] = arr[i];
            arr[i] = temp;
        }
    }

    public static void main(String[] args) {
        int[] arr = {64, 25, 12, 22, 11};
        selectionSort(arr);
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

2-3. 삽입 정렬 (Insertion Sort)

  • 원리: 데이터를 하나씩 정렬된 영역에 삽입.
  • 시간 복잡도:
    - 최악: O(N2N^2)
    - 최선: O(N) (거의 정렬된 경우)
  • 특징:
    - 작은 데이터셋에서 효율적이다.
    - 정렬이 거의 완료된 경우 유리하다.
  • 코드
public class InsertionSort {
    public static void insertionSort(int[] arr) {
        int n = arr.length;
        for (int i = 1; i < n; i++) {
            int key = arr[i];
            int j = i - 1;

            // arr[0..i-1]에서 key보다 큰 요소들을 한 칸씩 뒤로 밀기
            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key;  // key를 적절한 위치에 삽입
        }
    }

    public static void main(String[] args) {
        int[] arr = {12, 11, 13, 5, 6};
        insertionSort(arr);  // 배열 정렬 수행
        for (int num : arr) {
            System.out.print(num + " ");  // 정렬된 배열 출력
        }
    }
}

2-4. 병합 정렬 (Merge Sort)

  • 원리: 분할 정복(Divide and Conquer) 방법을 사용하여 배열을 반으로 나누고 각각을 정렬한 후 병합하는 방식입니다.
  • 일단 우리가 이해하고 있어야 할 점은 각각의 부분리스트는 '정렬된 상태'라는 점입니다. 두 부분리스트를 합쳐서 정렬할 때 굳이 삽입, 버블 정렬 등을 활용할 필요가 없고, 각 부분리스트의 첫 번째 원소부터 순차적으로 비교만 해주면 됩니다.
  • 시간복잡도: O(N log N)
  • 특징:
    - 안정 정렬.
    - 추가적인 메모리 공간 필요.
  • 코드
// 병합정렬
public class MergeSort {

    // 메인 메서드
    public static void main(String[] args) {
        int[] array = {21, 10, 12, 20, 25, 13, 15, 22};
        mergeSort(array, 0, array.length - 1);
        printArray(array);
    }

    // 병합 정렬 메서드
    public static void mergeSort(int[] array, int left, int right) {
        if (left < right) {
            int middle = (left + right) / 2;

            // 왼쪽 부분을 병합 정렬
            mergeSort(array, left, middle);

            // 오른쪽 부분을 병합 정렬
            mergeSort(array, middle + 1, right);

            // 병합
            merge(array, left, middle, right);
        }
    }

    // 병합 메서드
    public static void merge(int[] array, int left, int middle, int right) {
        int n1 = middle - left + 1;
        int n2 = right - middle;

        // 임시 배열 생성
        int[] leftArray = new int[n1];
        int[] rightArray = new int[n2];

        // 데이터 복사
        for (int i = 0; i < n1; i++) {
            leftArray[i] = array[left + i];
        }
        for (int j = 0; j < n2; j++) {
            rightArray[j] = array[middle + 1 + j];
        }

        // 병합
        int i = 0, j = 0;
        int k = left;
        while (i < n1 && j < n2) {
            if (leftArray[i] <= rightArray[j]) {
                array[k] = leftArray[i];
                i++;
            } else {
                array[k] = rightArray[j];
                j++;
            }
            k++;
        }
        // 남은 요소들 복사
        while (i < n1) {
            array[k] = leftArray[i];
            i++;
            k++;
        }
        while (j < n2) {
            array[k] = rightArray[j];
            j++;
            k++;
        }
    }
    // 배열을 출력하는 메서드
    public static void printArray(int[] array) {
        for (int i : array) {
            System.out.print(i + " ");
        }
        System.out.println();
    }
}

2-5. 퀵 정렬 (Quick Sort)

  • 원리: 2등분으로 나누고, pivot을 중심으로 작은값은 왼쪽 큰 값은 오른쪽으로 이동
  • 시간 복잡도:
    - 평균: O(N log N)
    - 최악: O(N2N^2) (Pivot 선택이 나쁠 경우)
  • 특징
    - 대부분의 경우 빠름.
    - 추가 메모리 공간 필요 없음 (in-place 정렬).
    -코드
public class QuickSort {
    // 퀵 정렬 메서드
    public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            // 분할 위치 계산
            int pi = partition(arr, low, high);

            // 분할된 부분 배열을 재귀적으로 정렬
            quickSort(arr, low, pi - 1);  // 피벗보다 작은 부분 정렬
            quickSort(arr, pi + 1, high); // 피벗보다 큰 부분 정렬
        }
    }

    // 분할 메서드 (피벗을 기준으로 배열을 나눔)
    public static int partition(int[] arr, int low, int high) {
        // 피벗은 배열의 마지막 요소
        int pivot = arr[high];
        int i = (low - 1);  // 작은 값의 인덱스를 추적

        // 배열을 순회하면서 피벗보다 작은 값을 앞쪽으로 이동
        for (int j = low; j < high; j++) {
            if (arr[j] < pivot) {
                i++; // 작은 값의 인덱스를 증가시킴
                // arr[i]와 arr[j]를 교환
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }

        // 피벗을 적절한 위치에 배치
        int temp = arr[i + 1];
        arr[i + 1] = arr[high];
        arr[high] = temp;

        // 피벗의 인덱스를 반환
        return i + 1;
    }

    // 메인 메서드
    public static void main(String[] args) {
        int[] arr = {10, 7, 8, 9, 1, 5};  // 정렬할 배열
        quickSort(arr, 0, arr.length - 1); // 퀵 정렬 호출
        // 정렬된 배열 출력
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

2-6. 힙 정렬 (Heap Sort)

  • 원리: 힙(Heap) 자료구조를 활용하여 최대값 또는 최소값을 뽑아 정렬. 힙 자료구조란 완전 이진 트리의 일종으로, 부모 노드가 자식 노드보다 항상 특정한 순서적 성질을 만족하는 트리입니다.
  • 시간 복잡도: O(N log N)
  • 특징
    - 안정 정렬 아님.
    - 추가 메모리 공간이 필요하지 않음.
  • 코드
public class HeapSort {
    // 힙 정렬 메서드
    public static void heapSort(int[] arr) {
        int n = arr.length;

        // 힙 생성 (최대 힙)
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapify(arr, n, i);  // 힙 속성 만족을 위한 재구성
        }

        // 힙에서 요소 추출
        for (int i = n - 1; i > 0; i--) {
            // 현재 루트를 배열 끝으로 이동 (최대값을 맨 뒤로 보냄)
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;

            // 크기가 줄어든 힙에 대해 다시 힙화
            heapify(arr, i, 0);
        }
    }

    // 힙화 메서드 (최대 힙으로 만들기)
    public static void heapify(int[] arr, int n, int i) {
        int largest = i; // 부모 인덱스
        int left = 2 * i + 1;  // 왼쪽 자식 인덱스
        int right = 2 * i + 2; // 오른쪽 자식 인덱스

        // 왼쪽 자식이 부모보다 크면 largest를 왼쪽 자식으로 변경
        if (left < n && arr[left] > arr[largest]) largest = left;

        // 오른쪽 자식이 부모 또는 왼쪽 자식보다 크면 largest를 오른쪽 자식으로 변경
        if (right < n && arr[right] > arr[largest]) largest = right;

        // 만약 부모가 가장 크지 않다면, 자식과 교환 후 다시 힙화
        if (largest != i) {
            int swap = arr[i];
            arr[i] = arr[largest];
            arr[largest] = swap;

            // 교환 후, 바뀐 자식에 대해 다시 힙화
            heapify(arr, n, largest);
        }
    }

    // 메인 메서드
    public static void main(String[] args) {
        int[] arr = {12, 11, 13, 5, 6, 7};  // 정렬할 배열
        heapSort(arr);  // 힙 정렬 호출
        // 정렬된 배열 출력
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

마무리

이번 공부를 통해 정렬에 대해 기본적인 개념과 구현 방법에 대해 알아보았습니다.
퀵 정렬과 힙 정렬 두 알고리즘에 대한 깊은 이해가 부족하여 이 주제에 대해서는 추가적인 공부가 필요할 것 같습니다. 정렬 알고리즘에 대한 이해를 더 깊에 쌓기 위해, 다양한 자료와 연습을 통해 이 부분을 보완할 예정입니다. 😊

profile
풀스택 개발자로서의 도전을 하는 중입니다. 많은 응원 부탁드립니다!!😁

0개의 댓글