7 minute read

1. 정렬

  • 정렬 : 순서가 없는 사물들을 순서(오름차순 또는 내림차순)대로 재배열하는 것
  • 정렬되어 있지 않은 자료에 대해서는 탐색의 효율성이 크게 떨어진다.
  • 레코드 : 정렬시켜야 할 대상
  • 키 : 여러 필드들 중에서 특정한 레코드를 식별해주는 역할을 하는 것
  • 정렬 : 레코드들을 키 값의 순서로 재배열하는 것

정렬 알고리즘의 분류

정렬 장소에 따른 분류

  • 내부 정렬 : 정렬하기 전에 모든 데이터가 메인 메모리에 올라와 있는 정렬
  • 외부 정렬 : 외부 기억 장치에 대부분의 데이터가 일부만 메모리에 올려놓은 상태에서 정렬하는 방법

구현 복잡도와 알고리즘 효율성에 따른 분류

  • 단순하지만 비효율적인 방법 : 삽입 정렬, 선택 정렬, 버블 정렬
  • 복잡하지만 효율적인 방법 : 퀵 정렬, 힙 정렬, 병합 정렬, 기수 정렬

안정성에 따른 분류

  • 안정성 : 입력 데이터에 동일한 키 값을 갖는 레코드가 여러 개 존재할 경우, 정렬 후에도 이들의 상대적인 위치가 바뀌지 않는 것
  • 안정성을 총족하는 정렬 : 삽입 정렬, 버블 정렬, 병합 정렬
비교 유무 방식 정렬 알고리즘
X 키 값에 따라 레코드 분배 기수 정렬
O 항목 교환 선택 정렬, 버블 정렬, 퀵 정렬
  적절한 삽입 삽입 정렬, 셸 정렬
  분할과 병합 병합 정렬

2. 선택 정렬

  • 전체 숫자들 중에서 가장 작은 숫자를 반복적으로 선택해 앞쪽으로 옮기는 방법
  • 제자리 정렬 : 입력 배열 이외에 추가적인 메모리가 필요하지 않는다.
  1. 전체 숫자들을 정렬된 부분(왼쪽 리스트)와 정렬되지 않은 부분(오른쪽 리스트)으로 나누고, 처음에는 모든 숫자를 오른쪽 리스트에 넣는다.
  2. 오른쪽 리스트에서 가장 작은 숫자를 선택하여 왼쪽 리스트로 순서대로 이동시킨다.
  3. 오른쪽 리스트가 공백 상태가 될 때까지 반복한다.
#include <stdio.h>
#define SWAP(x, y, t) ((t) = (x), (x) = (y), (y) = (t))

void printArray(int arr [], int n, int *str)
{
    int i;
    printf("%s = ", str);
    for (i = 0; i < n; i++) printf("%3d", arr[i]);
    printf("\n");
}

void printStep(int arr [], int n, int val)
{
    int i;
    printf("    Step %2d = ", val);
    for (i = 0; i < n; i++) printf("%3d", arr[i]);
    printf("\n");
}

void selection_sort(int list [], int n)
{
    int i, j, least, tmp;
    for (i = 0; i < n - 1; i++) {
        least = i;
        for (j = i + 1; j < n; j++)
            if (list[j] < list[least]) least = j;
        SWAP(list[i], list[least], tmp);
        printStep(list, n, i + 1);
    }
}

int main()
{
    int n = 9;
    int list[9] = {5, 3, 8, 4, 9, 1, 6, 2, 7};
    printArray(list, 9, "Original ");
    selection_sort(list, n);
    printArray(list, n, "Selection");
}

실행 결과

Original  =   5  3  8  4  9  1  6  2  7
    Step  1 =   1  3  8  4  9  5  6  2  7
    Step  2 =   1  2  8  4  9  5  6  3  7
    Step  3 =   1  2  3  4  9  5  6  8  7
    Step  4 =   1  2  3  4  9  5  6  8  7
    Step  5 =   1  2  3  4  5  9  6  8  7
    Step  6 =   1  2  3  4  5  6  9  8  7
    Step  7 =   1  2  3  4  5  6  7  8  9
    Step  8 =   1  2  3  4  5  6  7  8  9
Selection =   1  2  3  4  5  6  7  8  9

3. 삽입 정렬

정렬이 안 된 부분의 숫자를 정렬된 부분의 적절한 위치를 찾아 삽입하는 과정을 반복한다.

  1. 전체 숫자들을 정렬된 부분(왼쪽 리스트)와 정렬되지 않은 부분(오른쪽 리스트)으로 나누고, 처음에는 모든 숫자를 오른쪽 리스트에 넣는다.
  2. 정렬이 안 된 부분의 숫자를 정렬된 부분의 적절한 위치를 찾는다.
  3. 삽입할 항목보다 큰 항목들을 모두 한 칸씩 뒤쪽으로 이동시킨다.
  4. 숫자를 삽입한다.
  5. 이 과정을 정렬되지 않은 요소가 하나도 없을 때까지 반복한다.
#include <stdio.h>

void printArray(int arr [], int n, int *str)
{
    int i;
    printf("%s = ", str);
    for (i = 0; i < n; i++) printf("%3d", arr[i]);
    printf("\n");
}

void printStep(int arr [], int n, int val)
{
    int i;
    printf("    Step %2d = ", val);
    for (i = 0; i < n; i++) printf("%3d", arr[i]);
    printf("\n");
}

void insertion_sort(int list[], int n)
{
    int i, j, key;
    for (i = 1; i < n; i++) {
        key = list[i];
        for (j = i - 1; j >= 0 && list[j] > key; j--)
            list[j + 1] = list[j];
        list[j + 1] = key;
        printStep(list, n, i);
    }
}

int main()
{
    int n = 9;
    int list[9] = {5, 3, 8, 4, 9, 1, 6, 2, 7};
    printArray(list, 9, "Original ");
    insertion_sort(list, n);
    printArray(list, n, "Insertion");
}

실행 결과

Original  =   5  3  8  4  9  1  6  2  7
    Step  1 =   3  5  8  4  9  1  6  2  7
    Step  2 =   3  5  8  4  9  1  6  2  7
    Step  3 =   3  4  5  8  9  1  6  2  7
    Step  4 =   3  4  5  8  9  1  6  2  7
    Step  5 =   1  3  4  5  8  9  6  2  7
    Step  6 =   1  3  4  5  6  8  9  2  7
    Step  7 =   1  2  3  4  5  6  8  9  7
    Step  8 =   1  2  3  4  5  6  7  8  9
Insertion  =   1  2  3  4  5  6  7  8  9
  • 시간 복잡도 : $O(n^2)$
  • 레코드 양이 많거나 레코드 크기가 클 경우에 적합하지 않으며며, 레코드의 수가 적거나 대부분의 레코드가 이미 정렬되어 있는 경우에 효율적이다.

4. 버블 정렬

인접한 2개의 레코드를 비교하여 크기가 순서대로 되어 있지 않으면 서로 교환하는 방법

  1. 인접한 2개의 레코드를 비교하여 크기가 순서대로 되어 있지 않으면, 서로 교환한다.
  2. 리스트의 왼쪽 끝에서 오른쪽 끝으로 비교-교환 과정을 진행한다. (비교-교환 과정(스캔)이 완료되면, 가장 큰 레코드가 리스트의 오른쪽 끝으로 이동한다.)
  3. 더 이상 교환이 일어나지 않을 때까지 게속한다.
#include <stdio.h>
#define SWAP(x, y, t) ((t) = (x), (x) = (y), (y) = (t))

void printArray(int arr [], int n, int *str)
{
    int i;
    printf("%s = ", str);
    for (i = 0; i < n; i++) printf("%3d", arr[i]);
    printf("\n");
}

void printStep(int arr [], int n, int val)
{
    int i;
    printf("    Step %2d = ", val);
    for (i = 0; i < n; i++) printf("%3d", arr[i]);
    printf("\n");
}

void bubble_sort(int list [], int n)
{
    int i, j, bChanged, tmp;
    for (i = n - 1; i > 0; i--) {
        bChanged = 0;
        for (j = 0; j < i; j++)
            if (list[j] > list[j + 1]) {
                SWAP(list[j], list[j + 1], tmp);
                bChanged = 1;
            }
        if (!bChanged) break;
        printStep(list, n, n - i);
    }
}

int main()
{
    int n = 9;
    int list[9] = {5, 3, 8, 4, 9, 1, 6, 2, 7};
    printArray(list, 9, "Original ");
    bubble_sort(list, n);
    printArray(list, n, "Bubble ");
}

살행 결과

Original  =   5  3  8  4  9  1  6  2  7
    Step  1 =   3  5  4  8  1  6  2  7  9
    Step  2 =   3  4  5  1  6  2  7  8  9
    Step  3 =   3  4  1  5  2  6  7  8  9
    Step  4 =   3  1  4  2  5  6  7  8  9
    Step  5 =   1  3  2  4  5  6  7  8  9
    Step  6 =   1  2  3  4  5  6  7  8  9
Bubble  =   1  2  3  4  5  6  7  8  9
  • 시간 복잡도 : $O(n^2)$

5. 함수 포인터를 사용한 정렬

6. 셀 정렬

  1. 리스트를 일정한 기준에 따라 분류해 연속적이지 않은 여러 개의 부분 리스트로 만든다.
  2. 각 부분 리스트를 삽입 정렬을 이용하여 정렬한다.
  3. 모든 부분 리스트가 정렬되면, 셸 정렬은 다시 전체 리스트를 더 적은 개수의 부분 리스트로 만들어 앞의 과정을 반복한다.
  4. 이 과정을 부분 리스트의 개수가 1이 될 때까지 반복한다.
  • 간격 : 각 부분 리스트가 전체 리스트보부터 떨어진 거리

삽입 정렬에 비하여 셸 정렬은 2가지 장점

  1. 삽입 정렬에서는 한 번의 한 칸씩만 이동하는 반면, 연속적이지 않은 부분 파일에서 자료의 교환이 일어나면 더 큰 거리를 이동한다. 따라서 교환되는 아이템들이 삽입 정렬보다는 최종 위치에 더 가까이 있을 가능성이 높아진다.
  2. 부분 리스트가 하나가 되면 셸 정렬은 전체 리스트를 정렬해야 하지만, 거의 정렬된 리스트에 대한 삽입 정렬이므로 매우 효율적을 수행할 수 있다.

시간 복잡도

  • 최악의 경우 : $O(n^2)$
  • 평균적인 경우 : $O(n^{1.5})$

7. 병합 정렬

8. 퀵 정렬

  • 평균적으로 매우 빠르다.
  • 분할-정복법을 사용한다.
  1. 리스트 안에 있는 한 요소를 피벗으로 선택한다.
  2. 피벗보다 작은 요소들은 모두 피벗의 왼쪽으로 옯기고, 피벗보다 큰 요소들은 모두 피벗의 오른쪽으로 옮긴다.
  3. 피벗을 제외한 왼쪽 리스트와 오른쪽 리스트를 다시 정렬한다.
int partition(int list [], int left, int right)
{
    int tmp;
    int low = left + 1;
    int high = right;
    int pivot = list[left];
    while (low < high) {
        for (; low <= right && list[low < pivot; low++]);
            for (; high >= left && list[high] > pivot; high--);
                if (SWAP(list[low], list[high] > pivot; high--))
                    SWAP(list[low], list[high], tmp);
    }
    SWAP(list[left], list[high], tmp);
    return high;
}

9. 힙 정렬

10. 기수 정렬

11. 정렬 알고리즘 비교