코딩 테스트

정렬

조요피 2023. 1. 17. 20:49

정렬이란 데이터를 정해진 기준에 따라 배치해서 의미있는 구조로 재설정하는 것.

정렬 알고리즘 정의
버블 (bubble) 데이터의 인접 요소끼리 비교하고 swap 연산을 수행하여 정렬
선택 (selection) 대상에서 가장 크거나 작은 데이터를 찾아가 선택을 반복하면서 정렬
삽입 (insertion) 대상을 선택해 정렬된 영역에서 선택 데이터의 적절한 위치를 찾아 삽입하며 정렬
퀵 (quick) pivot 값을 선정해 해당 값을 기준으로 정렬
병합 (merge) 이미 정렬된 부분 집합들을 효율적으로 병합해 전체를 정렬하는 방식
기수 (radix) 데이터의 자릿수를 바탕으로 비교해 데이터를 정렬하는 방식

버블 정렬

버블 정렬은 두 인접한 데이터의 크기를 비교해 정렬하는 방법.

빅-세타 : O(n^2)

빅-오 : O(n^2)

// 포인트는 n - 1 - i
// 이때, i는 조건에서만 사용된다
// if 절에서는 j만 사용된다
// -1 하는 이유는 항상 다음 인덱스를 보기 때문
// -i 하는 이유는 이미 정렬된 항목은 안봐도 되기 때문
int temp;
for (int i = 0; i < n - 1; i++) {
    for (int j = 0; j < n - 1 - i; j++) {
        if (arr[j] > arr[j + 1]) {
            temp = arr[j];
            arr[j] = arr[j + 1];
            arr[j + 1] = temp;
        }
    }
}

선택 정렬

선택 정렬은 대상 데이터에서 최대나 최소 데이터를 데이터가 나열된 순으로 찾아가며 선택하는 방법.

빅-세타 : O(n^2)

빅-오 : O(n^2)

// arr.length - 1 인 이유는 마지막 인덱스까지 선택하지 않아도 되기 때문
// i + 1 인 이유는 선택된 인덱스는 제외하고 최소/최대 값을 찾으면 되기 때문
int[] arr = {1, 2, 3, 4};
int temp;
for (int i = 0; i < arr.length - 1; i++) {
    int minIndex = i;
    for (int j = i + 1; j < arr.length; j++) {
        if (arr[minIndex] < arr[j]) {
            minIndex = j;
        }
    }
    temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
}

// 4, 3, 2, 1

삽입 정렬

삽입 정렬은 이미 정렬된 데이터 범위에 정렬되지 않은 데이터를 적절한 위치에 삽입시켜 정렬하는 방식.

빅-세타 : O(n^2)

빅-오 : O(n^2)

// 삽입 정렬, 오름차순
int[] arr = {4, 2, 5, 7, 6, 3, 1, 9, 8, 0};
int n = arr.length;
for (int i = 1; i < n; i++) {
    int target = arr[i];
    int index = -1;
    for (int j = 0; j < i; j++) {
        if (arr[j] > target) {
            index = j;
            break;
        }
    }
    if (index != -1) {
        // shift
        for (int k = i; k > index; k--) {
            arr[k] = arr[k - 1];
        }
        arr[index] = target;
    }
}

퀵 정렬

퀵 정렬은 기준값(pivot)을 선정해 해당 값보다 작은 데이터와 큰 데이터로 분류하는 것을 반복해 정렬하는 알고리즘.

기준 값에 따라 시간 복잡도에 많은 영향을 미친다.

빅-세타 : O(nlogn)

빅-오 : O(n^2)

 

아래는 가장 왼쪽을 피벗으로 지정하는 코드.

투 포인터를 사용한다. (start, end)

1.왼쪽에서 오른쪽으로 가면서 피벗보다 큰 값을 찾는다. (start)

2.오른쪽에서 왼쪽으로 가면서 피벗보다 작은 값을 찾는다. (end)

3.데이터를 찾았다면 두 데이터의 값을 바꾼다.

4.start와 end가 서로를 지나칠때 까지 1~3 을 반복한다.

5.start와 end가 서로를 지나치면 end와 피벗의 값을 바꾼다.

-> 이때 피벗을 기준으로 왼쪽은 피벗보다 작은 값, 오른쪽은 피벗보다 큰 값이 된다.

-> 이렇게 하기 위하여 모든 과정이 있는 것!

6.피벗을 기준으로 좌측 배열과 우측 배열을 1~5 를 반복하여 정렬한다.

/**
 * 퀵 정렬
 * @param arr 정렬할 배열
 * @param start 정렬할 범위의 처음
 * @param end 정렬할 범위의 마지막
 */
public static void quickSort(int[] arr, int start, int end) {
    int pivot = start; // 피벗 설정, 항상 제일 왼쪽 값으로 설정
    int head = start; // start 값은 변하기 때문에 초기값 저장
    int tail = end; // end 값도 변하기 때문에 초기값 저장

    int temp;
    // start와 end는 계속해서 증가하거나 감소하는데 서로를 지나치면 루프를 멈춘다.
    while (start < end) {
        // start와 end 값은 지정된 범위를 벗어나면 안된다.
        // head와 tail 사이에 있어야 한다.
        while (start < tail && arr[pivot] >= arr[start]) start++; // 피벗보다 큰 값을 만날때까지 start 증가
        while (end > head && arr[pivot] <= arr[end]) end--; // 피벗보다 작은 값을 만날때까지 end 감소

        // 여기까지 왔다면 피벗보다 작은 값과 큰값에 대한 인덱스가 설정된 상태
        //swap
        if (start < end) {
            temp = arr[start];
            arr[start] = arr[end];
            arr[end] = temp;
        }
    }

    // 여기까지 왔다면 루프를 빠져나온 상태
    // start와 end가 서로 지나친 상태
    // end 인덱스의 위치에 피벗값을 넣어준다.
    temp = arr[pivot];
    arr[pivot] = arr[end];
    arr[end] = temp;

    // 피벗을 기준으로 왼쪽 데이터 정렬
    if (head < end - 1) quickSort(arr, head, end - 1);
    // 피벗을 기준으로 오른쪽 데이터 정렬
    if (end + 1 < tail) quickSort(arr, end + 1, tail);
}

병합 정렬

병합 정렬은 분할 정복(divide and conquer) 방식을 사용해 데이터를 분할하고 분할한 집합을 정렬하며 합치는 알고리즘.

빅-세타 : O(nlogn)

빅-오 : O(nlogn)

/**
 * 병합 정렬
 * @param arr 정렬할 배열
 * @param head 정렬할 배열의 첫번째 인덱스
 * @param tail 정렬할 배열의 마지막 인덱스
 */
public void mergeSort(int[] arr, int head, int tail) {
    if (head < tail) {
        int mid = (head + tail) / 2; // 중간값 설정
        mergeSort(arr, head, mid); // 중간값 기준으로 왼쪽
        mergeSort(arr, mid + 1, tail); // 중간값 기준으로 오른쪽

        // 여기까지 도달했다면 최소 단위로 나누는 작업은 모두 끝남
        // 이제 중간 값을 기준으로 좌우 배열을 비교하여 정렬할 차례

        int[] temp = new int[arr.length]; // 정렬된 값을 담을 임시 배열
        int left = head; // 중앙값 기준으로 왼쪽 배열 첫번째 인덱스
        int right = mid + 1; // 중앙값 기준으로 오른쪽 배열 첫번째 인덱스
        int index = head; // 정렬된 값을 차례로 넣을 인덱스 설정
        while (left <= mid || right <= tail) {
            // 아래 조건이 중요
            // right > tail : 오른쪽 배열값이 모두 정렬됐다면 왼쪽 값을 차례로 삽입
            // left <= mid : right가 tail을 넘어가는 상황도 left가 mid를 넘어가는 상황도 발생하기 때문에 조건을 걸어줌
            if (right > tail || (left <= mid && arr[left] <= arr[right])) {
                temp[index++] = arr[left++];
            } else {
                temp[index++] = arr[right++];
            }
        }

        // 임시로 정렬된 값을 실제 배열에 넣어줌
        for (int i = head; i <= tail; i++) {
            arr[i] = temp[i];
        }
    }
}

기수 정렬

기수 정렬(radix sort)는 값을 비교하지 않는 특이한 정렬.

값을 놓고 비교할 자릿수를 정한 다음 해당 자릿수만 비교.

모든 경우의 시간 복잡도 : O(n)

public static void radixSort(int[] arr) {
    Queue<Integer>[] qa = new LinkedList[10];

    for (int i = 0; i < 10; i++) {
        qa[i] = new LinkedList<>();
    }

    int jarisu = 1; // 1 -> 10 -> 100 으로 늘어남. 1의자리 10의자리 100의자리.
    int max_jarisu = 5; // 숫자의 최대 자리수. ex) 123 -> 3자리, 52344 -> 5자리

    for (int i = 0; i < max_jarisu; i++) {
        // 자리수를 기준으로 값들을 나누어 담음
        for (int j = 0; j < arr.length; j++) {
            qa[(arr[j] / jarisu) % 10].add(arr[j]); // 값을 자리수로 나눠서 %10 연산을 하는 것이 핵심!
        }

        // 자리수로 나누어진 값들을 순서대로 꺼내서 배열에 삽입
        for (int j = 0, k = 0; j < arr.length; j++) {
            while (qa[k].peek() == null) {
                k++;
            }
            arr[j] = qa[k].poll();
        }

        jarisu *= 10;
    }
}

 

'코딩 테스트' 카테고리의 다른 글

그리디  (0) 2023.01.31
탐색  (0) 2023.01.19
스택과 큐  (0) 2023.01.11
배열과 리스트  (0) 2023.01.11
시간 복잡도와 표기법  (0) 2023.01.01