C 언어에서 정렬 알고리즘 (버블 정렬, 선택 정렬, 삽입 정렬) 구현하기

제공

정렬 알고리즘, 프로그래밍의 기초이자 가장 중요한 개념 중 하나입니다. 데이터를 효율적으로 관리하고 활용하기 위한 필수적인 도구죠. 이 글에서는 C 언어를 통해 버블 정렬, 선택 정렬, 삽입 정렬, 이 세 가지 기본 정렬 알고리즘을 자세히 살펴보고 직접 구현해보겠습니다. 각 알고리즘의 작동 원리를 이해하고 코드로 표현하는 과정을 통해 정렬 알고리즘에 대한 깊이 있는 이해를 쌓을 수 있을 것입니다. 단순히 코드를 복사-붙여넣기 하는 것이 아니라, 각 알고리즘의 핵심 로직을 파악하여 응용력을 키우는 데 집중합니다. 마지막으로 세 가지 정렬 알고리즘을 비교 분석하여 각각의 장단점을 파악하고 상황에 맞는 최적의 알고리즘을 선택하는 방법까지 알아보겠습니다. 지금 바로 C 언어 정렬 알고리즘의 세계로 떠나볼까요?

 

 

버블 정렬 알고리즘 이해와 구현

버블 정렬! 이름만 들어도 왠지 귀엽지 않나요? 마치 비눗방울처럼 데이터들이 둥둥 떠다니면서 자기 자리를 찾아가는 모습이 상상되는 것 같아요~ ^^ 하지만 그 귀여운 이름 뒤에는 놀랍도록 체계적인 정렬 방식이 숨어있답니다. 버블 정렬은 인접한 두 원소를 비교하고, 순서가 잘못되어 있으면 자리를 바꾸는 방식으로 동작해요. 이 과정을 리스트의 끝까지 반복하면서, 마치 거품처럼 가장 큰 값이 맨 위로 올라오는 모습을 볼 수 있죠.

버블 정렬 알고리즘

자, 그럼 이 매력적인 버블 정렬 알고리즘을 좀 더 깊이 파헤쳐 볼까요? C 언어를 통해 직접 구현해보면서 그 원리를 더욱 명확하게 이해할 수 있을 거예요!

예시 배열

먼저, 간단한 예시 배열을 생각해 봅시다. arr = [5, 1, 4, 2, 8] 이라는 배열이 있다고 가정해 보죠. 버블 정렬은 첫 번째 원소 5와 두 번째 원소 1을 비교합니다. 5가 1보다 크므로 자리를 바꿔 [1, 5, 4, 2, 8]이 됩니다. 다음으로 5와 4를 비교, 5가 4보다 크므로 다시 자리를 바꿔 [1, 4, 5, 2, 8]이 됩니다. 이런 식으로 비교와 교환을 반복하며 한 사이클이 끝나면 가장 큰 값인 8이 맨 마지막 자리로 이동하게 되죠!

C 코드 구현

이러한 과정을 C 코드로 표현하면 다음과 같습니다.

#include <stdio.h>

void bubble_sort(int arr[], int n) {
int i, j, temp;
for (i = 0; i < n - 1; i++) { // n-1번 반복하면 정렬 완료!
for (j = 0; j < n - i - 1; j++) { // 각 패스에서 비교 범위가 줄어듦에 주목!
if (arr[j] > arr[j + 1]) { // 현재 원소가 다음 원소보다 크면?!
// 두 원소 교환 (swap)!!
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}

int main() {
int arr[] = {5, 1, 4, 2, 8};
int n = sizeof(arr) / sizeof(arr[0]); // 배열 크기 계산!
bubble_sort(arr, n); // 버블 정렬 함수 호출!
printf("정렬된 배열: ");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n"); // 개행 추가
return 0;
}

코드 설명

코드에서 주목할 점은 바깥쪽 루프(i)는 배열의 크기보다 하나 작은 만큼 반복한다는 것입니다. 왜냐하면 마지막 원소는 이미 정렬되었을 것이기 때문이죠! 안쪽 루프(j)는 n-i-1까지 반복하는데, 이는 이미 정렬된 부분을 다시 비교할 필요가 없기 때문입니다. 효율성을 위한 깨알 팁이랄까요? ^^

시간 복잡도

버블 정렬의 시간 복잡도는 평균적으로 O(n²)입니다. n이 커질수록 실행 시간이 급격하게 증가하죠. 즉, 데이터의 양이 많아지면 효율이 떨어진다는 의미입니다. 하지만 코드가 간결하고 구현이 쉬워 교육적인 목적으로 많이 사용됩니다. 또한, 이미 정렬된 배열에 대해서는 최적의 성능인 O(n)을 보여준다는 장점도 있답니다!

마무리

자, 이제 버블 정렬에 대해 조금 더 감이 잡히시나요? 다음에는 선택 정렬 알고리즘에 대해 알아보도록 하겠습니다! 기대해주세요~

 

선택 정렬 알고리즘 이해와 구현

버블 정렬을 배우셨으니 이제 선택 정렬이라는 녀석을 만나볼 시간입니다! 선택 정렬은 이름에서 짐작할 수 있듯이 ‘선택’이 핵심인 정렬 알고리즘입니다. 어떤 선택일까요? 바로 주어진 데이터 중에서 최솟값이나 최댓값을 ‘선택’하는 것이죠! 마치 보물찾기처럼 말이에요. 자, 그럼 이 보물찾기가 어떻게 데이터 정렬로 이어지는지 한번 살펴볼까요?

선택 정렬의 과정

선택 정렬은 크게 두 가지 과정이 반복됩니다. 첫 번째는 정렬되지 않은 부분에서 최솟값(또는 최댓값)을 찾는 과정이고, 두 번째는 찾은 최솟값을 정렬된 부분의 끝에 놓는 과정입니다. 이 두 단계가 마치 춤을 추듯 우아하게 반복되면서 데이터가 정렬되는 마법이 펼쳐지는 거죠! 마치 카드 게임에서 가장 낮은 숫자의 카드를 찾아 왼쪽으로 옮기는 것을 반복하는 것과 비슷합니다. 신기하지 않나요?

선택 정렬의 예시

자, 이제 좀 더 구체적으로 들어가 봅시다. 예를 들어, [5, 2, 8, 1, 9, 4] 이렇게 정렬되지 않은 6개의 숫자가 있다고 가정해 볼게요. 선택 정렬은 먼저 이 숫자들 중 가장 작은 숫자인 1을 찾습니다. 그리고 이 1을 맨 앞의 5와 자리를 바꿉니다. 그러면 [1, 2, 8, 5, 9, 4]가 되겠죠? 다음으로는 1을 제외한 나머지 숫자들 [2, 8, 5, 9, 4] 중 가장 작은 숫자인 2를 찾습니다. 이미 2가 두 번째 자리에 있으므로 자리 바꿈은 필요 없겠네요! 이 과정을 계속 반복하면 결국 [1, 2, 4, 5, 8, 9]처럼 완벽하게 정렬된 순서를 얻게 됩니다!

C 코드 구현

코드로 구현하는 것도 어렵지 않습니다. C 언어를 사용하면 다음과 같이 선택 정렬을 구현할 수 있습니다.

#include <stdio.h>

void selection_sort(int arr[], int n) {
    int i, j, min_idx;

    // 바깥쪽 루프: 정렬되지 않은 부분의 시작 인덱스
    for (i = 0; i < n-1; i++) {
        // 최솟값의 인덱스를 현재 인덱스로 초기화
        min_idx = i;

        // 안쪽 루프: 현재 인덱스 다음부터 끝까지 최솟값 탐색
        for (j = i+1; j < n; j++)
          if (arr[j] < arr[min_idx])
            min_idx = j;

        // 최솟값을 현재 위치로 이동 (swap)
        if (min_idx != i) {  //현재 위치에 최솟값이 아닌 경우만 swap 진행!
            int temp = arr[i];
            arr[i] = arr[min_idx];
            arr[min_idx] = temp;
        }
    }
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 4};
    int n = sizeof(arr)/sizeof(arr[0]);
    int i;

    selection_sort(arr, n); //선택 정렬 함수 호출

    printf("정렬된 배열: \n");
    for (i=0; i < n; i++)
        printf("%d ", arr[i]);  //정렬된 배열 출력!
    printf("\n");

    return 0;
}

코드 설명

코드를 보면 이중 반복문이 사용되는 것을 알 수 있습니다. 바깥쪽 반복문은 정렬되지 않은 부분의 시작 위치를 나타내고, 안쪽 반복문은 최솟값을 찾는 역할을 합니다. min_idx 변수는 현재까지 찾은 최솟값의 인덱스를 저장하고, 반복문이 끝나면 swap을 통해 최솟값을 정렬된 위치로 이동시킵니다. 마치 퍼즐 조각을 맞추는 것처럼 말이죠!

선택 정렬의 시간 복잡도

선택 정렬의 시간 복잡도는 O(n^2)입니다. 데이터의 양이 많아질수록 실행 시간이 기하급수적으로 늘어날 수 있다는 의미죠. 하지만 데이터의 양이 적거나, 이미 어느 정도 정렬되어 있는 경우에는 비교적 효율적일 수 있습니다. 또한, 추가적인 메모리 공간을 거의 사용하지 않는다는 장점도 가지고 있습니다. 다음에는 삽입 정렬에 대해 알아보겠습니다. 기대해주세요!

 

삽입 정렬 알고리즘 이해와 구현

자, 이번에는 드디어 삽입 정렬 알고리즘 차례입니다! 버블 정렬과 선택 정렬을 잘 이해하셨다면 삽입 정렬도 어렵지 않게 따라오실 수 있을 거예요! 삽입 정렬은 마치 카드 게임에서 손에 들고 있는 카드를 순서대로 정렬하는 것과 같은 방식으로 작동합니다. 새로운 카드를 받으면 이미 정렬된 카드 사이에 적절한 위치를 찾아 삽입하는 거죠! 흥미롭지 않나요?!

삽입 정렬의 효율성

삽입 정렬은 특히 데이터가 거의 정렬되어 있거나 데이터의 크기가 작을 때 매우 효율적입니다. 이 알고리즘의 시간 복잡도는 최악의 경우 O(n²)이지만, 최선의 경우 (이미 정렬된 경우) O(n)으로 매우 빠릅니다. 평균 시간 복잡도는 O(n²)이며, 안정 정렬에 속합니다. 안정 정렬이란, 같은 값을 가진 원소들의 상대적인 순서가 정렬 후에도 유지되는 것을 의미합니다. 이 부분, 놓치지 마세요~!!

삽입 정렬의 작동 방식

자, 그럼 삽입 정렬의 작동 방식을 좀 더 자세히 살펴볼까요? 핵심 아이디어는 이미 정렬된 부분과 정렬되지 않은 부분을 나누는 것입니다. 처음에는 첫 번째 원소 하나만 정렬된 부분으로 간주하고, 나머지는 정렬되지 않은 부분으로 봅니다. 그런 다음 정렬되지 않은 부분에서 하나씩 원소를 꺼내서 이미 정렬된 부분의 적절한 위치에 삽입하는 과정을 반복합니다. 마치 카드 게임 같죠? ^^

삽입 정렬 예시

예를 들어, [5, 2, 4, 6, 1, 3] 배열을 삽입 정렬로 정렬하는 과정을 단계별로 살펴보겠습니다.

  1. 첫 번째 원소 (5)는 이미 정렬된 것으로 간주합니다. 현재 상태: [5, 2, 4, 6, 1, 3] (정렬된 부분: [5])
  2. 두 번째 원소 (2)를 삽입합니다. 2는 5보다 작으므로 5 앞에 삽입합니다. 현재 상태: [2, 5, 4, 6, 1, 3] (정렬된 부분: [2, 5])
  3. 세 번째 원소 (4)를 삽입합니다. 4는 5보다 작고 2보다 크므로 2와 5 사이에 삽입합니다. 현재 상태: [2, 4, 5, 6, 1, 3] (정렬된 부분: [2, 4, 5])
  4. 네 번째 원소 (6)를 삽입합니다. 6은 5보다 크므로 그대로 뒤에 삽입합니다. 현재 상태: [2, 4, 5, 6, 1, 3] (정렬된 부분: [2, 4, 5, 6])
  5. 다섯 번째 원소 (1)을 삽입합니다. 1은 2보다 작으므로 맨 앞에 삽입합니다. 현재 상태: [1, 2, 4, 5, 6, 3] (정렬된 부분: [1, 2, 4, 5, 6])
  6. 여섯 번째 원소 (3)를 삽입합니다. 3은 6보다 작고, 5보다 작고, 4보다 작고, 2보다 크고 1보다 크므로 2와 4 사이에 삽입합니다. 최종 상태: [1, 2, 3, 4, 5, 6] (정렬된 부분: [1, 2, 3, 4, 5, 6])

이처럼 삽입 정렬은 정렬되지 않은 부분에서 원소를 하나씩 꺼내 정렬된 부분의 적절한 위치에 삽입하는 과정을 반복하여 전체 배열을 정렬합니다. 이해가 되셨나요? 아직 헷갈리는 부분이 있다면 다시 한번 천천히 읽어보세요!

C 언어로 삽입 정렬 구현

자, 그럼 이제 C 언어로 삽입 정렬을 구현해 보겠습니다. 아래 코드를 참고해 주세요!


#include <stdio.h>

void insertion_sort(int arr[], int n) {
    int i, key, j;
    for (i = 1; i < n; i++) {
        key = arr[i]; // 현재 삽입할 원소
        j = i - 1; // 이미 정렬된 부분의 마지막 인덱스

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

int main() {
    int arr[] = {5, 2, 4, 6, 1, 3};
    int n = sizeof(arr) / sizeof(arr[0]);

    insertion_sort(arr, n);

    printf("정렬된 배열: \n");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

위 코드는 insertion_sort 함수를 사용하여 배열을 삽입 정렬로 정렬하는 예제입니다. main 함수에서는 예시 배열을 정렬하고 결과를 출력합니다. 코드를 직접 실행해 보면서 삽입 정렬의 과정을 눈으로 확인해 보는 것을 추천합니다! 더 나아가 다른 배열을 사용하여 테스트해보고, 코드를 수정하며 삽입 정렬 알고리즘에 대한 이해를 깊게 해보세요! 다음에는 세 가지 정렬 알고리즘을 비교 분석해보는 시간을 갖겠습니다. 기대해주세요!

 

세 가지 정렬 알고리즘 비교 분석

지금까지 버블 정렬, 선택 정렬, 삽입 정렬, 이렇게 세 가지 정렬 알고리즘에 대해 자세히 알아봤는데요! 각각의 알고리즘은 나름의 매력(?)을 가지고 있지만, 실제로 어떤 상황에서 어떤 알고리즘을 사용하는 것이 좋을까요? 알고리즘의 성능을 객관적으로 비교하고 분석하는 시간을 가져보겠습니다! 두근두근?!

시간 복잡도

가장 먼저 시간 복잡도를 살펴보겠습니다. 시간 복잡도는 입력 데이터의 크기(n)에 따라 알고리즘의 실행 시간이 어떻게 변하는지를 나타내는 척도입니다. Big O 표기법을 사용하면, 최악의 경우, 평균적인 경우, 그리고 최선의 경우에 대한 시간 복잡도를 간결하게 표현할 수 있죠.

알고리즘 최악의 경우 평균적인 경우 최선의 경우
버블 정렬 O(n²) O(n²) O(n)
선택 정렬 O(n²) O(n²) O(n²)
삽입 정렬 O(n²) O(n²) O(n)

표에서 볼 수 있듯이, 세 가지 알고리즘 모두 최악의 경우와 평균적인 경우 시간 복잡도가 O(n²)입니다. 즉, 데이터의 크기가 두 배로 늘어나면 실행 시간은 네 배로 증가할 수 있다는 의미죠! 하지만 최선의 경우를 보면 버블 정렬과 삽입 정렬은 O(n)의 시간 복잡도를 보입니다. 이미 정렬된 데이터를 다시 정렬할 때는 훨씬 빠르게 동작한다는 것을 알 수 있습니다. 선택 정렬은 최선의 경우에도 O(n²)의 시간 복잡도를 유지하는데, 이는 매번 최솟값을 찾기 위해 전체 배열을 탐색해야 하기 때문입니다. (으악!)

공간 복잡도

다음으로 공간 복잡도를 살펴보겠습니다. 공간 복잡도는 알고리즘이 실행되는 동안 필요한 추가 메모리 공간의 양을 나타냅니다. 세 가지 알고리즘 모두 입력 데이터 외에 추가적인 메모리 공간을 거의 사용하지 않으므로, 공간 복잡도는 O(1)입니다. 즉, 데이터의 크기와 관계없이 일정한 양의 메모리만 사용한다는 뜻이죠! (와우!)

알고리즘 공간 복잡도
버블 정렬 O(1)
선택 정렬 O(1)
삽입 정렬 O(1)

각 알고리즘의 특징

이러한 시간 복잡도와 공간 복잡도를 바탕으로 각 알고리즘의 특징을 정리해보면 다음과 같습니다.

  • 버블 정렬: 인접한 두 요소를 비교하고 교환하는 방식으로 동작합니다. 구현이 간단하지만, 성능은 세 가지 알고리즘 중 가장 떨어집니다. 이미 거의 정렬된 데이터에 대해서는 효율적일 수 있습니다.
  • 선택 정렬: 매번 최솟값을 찾아서 앞으로 이동시키는 방식으로 동작합니다. 버블 정렬보다 약간 빠르지만, 여전히 성능은 좋지 않습니다. 데이터의 크기가 작을 때 사용하기에 적합합니다.
  • 삽입 정렬: 현재 요소를 이미 정렬된 부분에 삽입하는 방식으로 동작합니다. 세 가지 알고리즘 중 성능이 가장 좋으며, 데이터의 크기가 작거나 거의 정렬된 경우에 특히 효율적입니다.

알고리즘 선택 기준

자, 이제 실제 상황을 가정해 보겠습니다. 만약 정렬해야 할 데이터의 크기가 매우 크다면 (예를 들어, 수백만 개의 데이터!), O(n log n)의 시간 복잡도를 가지는 병합 정렬이나 퀵 정렬과 같은 고급 정렬 알고리즘을 사용하는 것이 좋습니다. 하지만 데이터의 크기가 작고, 구현의 단순함이 중요하다면, 버블 정렬, 선택 정렬, 삽입 정렬 중 하나를 선택할 수 있습니다. 특히 데이터가 거의 정렬되어 있는 경우라면 삽입 정렬이 좋은 선택이 될 수 있겠죠?!

정렬 알고리즘은 각각 장단점을 가지고 있기 때문에 상황에 맞는 알고리즘을 선택하는 것이 중요합니다. 데이터의 크기, 정렬 상태, 그리고 구현의 복잡성 등을 고려하여 최적의 알고리즘을 선택해야 합니다. 이번 포스팅을 통해 정렬 알고리즘에 대한 이해를 높이고, 실제 프로그래밍에 적용하는 데 도움이 되었으면 좋겠습니다! (짝짝짝!) 앞으로 더욱 다양한 알고리즘들을 탐구하고, 효율적인 코드를 작성하는 여정을 함께해요!

 

지금까지 C 언어로 구현한 버블 정렬, 선택 정렬, 삽입 정렬에 대해 살펴보았습니다. 각 알고리즘은 서로 다른 방식으로 동작하며, 효율성 또한 상황에 따라 달라집니다. 작은 데이터셋을 다룰 때는 세 알고리즘 모두 큰 차이 없이 빠른 성능을 보여주지만, 데이터의 양이 증가할수록 알고리즘 선택에 따른 성능 차이가 두드러지게 됩니다. 삽입 정렬은 이미 어느 정도 정렬된 데이터에 효율적이며, 버블 정렬단순함이 장점입니다. 선택 정렬데이터 이동 횟수가 적다는 특징을 가집니다. 이러한 차이점을 이해하고 상황에 맞는 알고리즘을 선택하는 것이 효율적인 프로그래밍의 핵심입니다. 앞으로 더욱 복잡한 정렬 알고리즘을 배우기 위한 탄탄한 기초를 다지셨기를 바랍니다.


코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다