정렬 알고리즘

정렬 알고리즘은 알고리즘 과목 중에서 기초적으로 반드시 알고 지나가야되는 파트입니다. 단순히 작은 순, 또는 큰 순으로 순서를 정렬하는 데에도 여러가지 알고리즘이 존재하는데요. 우리가 바로바로 이해할 수 있는 알고리즘이 있는 가 하면 좀 복잡하게 정렬하는 알고리즘도 있지요. 우리는 그나마 쉬운 편에 속하는 정렬 알고리즘 3개를 같이 보겠습니다.

 

1. 선택정렬(Selection Sort)

선택 정렬은 일단 자리는 정해져있습니다. 첫번째 자리에 가장 작은 녀석을 집어넣으면 되죠. 그리고 난 후에 두번째 자리에 그 다음 가장 작은 녀석을 선택해 집어 넣습니다. 이 짓을 배열이 끝날때까지 합니다.

 

다음 배열 arr에는 9, 8, 1, 2, 3이 있습니다. 이것을 선택정렬로 정렬하도록 해봅시다.

 

 

우선 제일 첫번째 자리 , 즉 9가 있는 자리를 i라고 놓고 j는 i 다음부터 배열의 가장 작은 index를 뽑아 옵니다. 그것을 minIndex라고 하지요.

j는 8부터 배열을 검색하면서 가장 작은 index를 찾아보니 1이 있는 자리가 제일 작군요. 그러니 minIndex는 2(1이 있는 배열 원소의 위치!)가 됩니다. 그래서 가장 첫번째 자리는 1이 되겠습니다.

 

 

 

그 후 두번째 자리(8이 있는 자리)가 i가 되고, j는 그다음 가장 작은 원소의 index를 구하게 되니까 minIndex는 2가 있는 자리, 즉 4가 됩니다.

minIndex와 i를 바꿉니다.

 

 

 

다음은 9가 있는 자리입니다. i는 2가 되겠네요. 가장 작은 인덱스는 3이 있는 자리이므로 i와 바꾸어 줍니다.

 

 

다음 i가 하나 증가되어 9를 가리킵니다. 8이 더 작으니까 바꾸어 주어야겠죠??

 

 

이제 모든 비교와 교환은 끝났습니다. 오름차순으로 정렬이 된것을 볼 수 있습니다.

 

 

 

아래는 선택정렬을 구현한 코드입니다.

void selectionSort(int arr[], int size) {
    int minIndex;
    int i, j;
    for (i = 0; i < size - 1; i++) {
        minIndex = i;
        for (j = i + 1; j < size; j++)
            if (arr[j] < arr[minIndex])
                minIndex = j;

        swap(&arr[i], &arr[minIndex]);
    }
}
 
2. 버블 정렬
버블 정렬은 현재 배열 요소와 그 다음 배열 요소를 비교한다음 조건에 걸리면 교환하는 식의 정렬입니다. 그러니까 배열의 0번 인덱스의 요소와 1번 인덱스의 요소를 비교하고, 그 다음 1번 인덱스의 요소와 2번 인덱스의 요소를 비교합니다. 이 짓을 계속하면 정렬이 됩니다. 아래의 배열을 가지고 버블 정렬로 정렬해보도록 하지요.
 

 

 

인덱스를 j라고 표시하겠습니다. 우선 가장 앞 자리부터 그 다음 원소를 비교합니다. 9와 8을 우선 비교하는 것이죠. 9가 더 크니 8과 9를 바꿉시다. 

 

 

 

그 다음 j가 하나 증가하여 다음 자리를 가리키고 9와 1을 비교합니다. 9가 더 크니 1과 9를 바꿉시다.

 

느낌 오시나요? 가장 큰 요소를 끝으로 보내려고 하는 겁니다. 다음 j를 하나 증가시키고 9와 3을 비교합니다. 9가 더크네요. 3과 9를 바꾸어줍니다.

 

 

다시 j가 하나 증가하게 되고 9과 2를 비교해보니 9가 더 크니까 자리를 바꾸어줍니다.

 

이렇게 보니까 9(제일 큰 수)가 제일 뒤로 갔군요. 

 

 

 

그 다음은 4번째(인덱스 3)까지 이런 방식을 반복합니다. 하나씩 줄여가면서 이런 방식을 반복하여 가장 큰수를 오른쪽으로 보내면 정렬이 되는 것이죠. 버블 정렬을 코드로 구현한 것이 아래에 있습니다.

void bubbleSort(int arr[], int size) {
    int i, j;
    for (i = size - 1; i > 0; i--)
        for (j = 0; j < i; j++)
            if (arr[j] > arr[j + 1])
                swap(&arr[j], &arr[j + 1]);
}

 

 

3. 삽입 정렬

 

삽입 정렬을 우선 배열의 한 원소인 key라는 값을 우선 가지고 있고, 이 key를 알맞은 자리에 삽입하면 됩니다. key보다 큰 값은 하나하니씩 밀어버리고 key보다 작은 값을 만났으때 그 뒷자리에 삽입하는 것입니다.

 

그림을 통해봅시다.

우선 배열이 이렇게 있구요. 사실 계속 이 배열을 썼었죠.

 

 

우선 key값은 배열 인덱스 1번부터 시작입니다. i 이전의 원소를 비교해야하는데, 인덱스가 0이면 비교할게 없으니까요. key는 8로 8보다 큰 값은 오른쪽으로 밀어버립니다. 8보다 작은 원소가 발견되면 그 원소 뒤에 8을 삽입해야하는 데 없으므로 제일 앞에 삽입합니다.

 

 

 

8이 맨앞에 삽입 되고 난 후 i가 하나 증가하여 계속 비교합니다. key는 1이 됩니다. 1보다 작은 원소가 없으므로 제일 앞에 위치하게 됩니다.

 

 

 

 

 

이후 i가 하나 증가하고 키는 3입니다. 3보다 작은 원소를 만날때 그 뒤에 삽입하는 과정을 아래 그림이 보여줍니다.

 

 

 

 

 

 

 

이제 i가 하나 증가하게 되고 마지막 원소인 2가 키가 됩니다. 2보다 작은 원소는 1이므로 1뒤에 2를 삽입하는 것이 되지요. 아래 그림이 그 과정을 보여줍니다.

 

 

 

 

 

 

 

 

 

 

 

최종적으로 정렬된 상태는 바로 이와 같습니다.

 

 

삽입 정렬에 대한 코드는 아래와 같습니다.

void insertionSort(int arr[], int size) {
    int i, j, key;

    for (i = 1; i < size; i++) {
        key = arr[i];
        j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

 

시간복잡도

선택 정렬과 버블 정렬은 항상 n^2의 시간복잡도를 갖지만 삽입정렬은 조금 다릅니다. 삽입 정렬은 평균적으로 n^2이지만 만약 정렬된 원소들로 구성된 배열이 입력으로 들어오면 n^2의 시간 복잡도를 갖습니다. for 루프안의 반복문이 1회만 실시하고 말거든요. 바로 현재 키보다 작은 원소를 만나기 때문입니다. 아래의 표로 쉽게 확인해보세요.

 

정렬 알고리즘   최적의 경우 평균적인 경우  최악의 경우 
 Selection Sort  n^2  n^2  n^2
 Bubble Sort  n^2  n^2  n^2
 Insertion Sort  n  n^2  n^2

 

 

전체코드

확인하기 쉽도록 전체코드를 올립니다.

#include <stdio.h>

void swap(int* a, int* b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
void selectionSort(int arr[], int size) {
    int minIndex;
    int i, j;
    for (i = 0; i < size - 1; i++) {
        minIndex = i;
        for (j = i + 1; j < size; j++)
            if (arr[j] < arr[minIndex])
                minIndex = j;

        swap(&arr[i], &arr[minIndex]);
    }
}
void bubbleSort(int arr[], int size) {
    int i, j;
    for (i = size - 1; i > 0; i--)
        for (j = 0; j < i; j++)
            if (arr[j] > arr[j + 1])
                swap(&arr[j], &arr[j + 1]);
}

void insertionSort(int arr[], int size) {
    int i, j, key;

    for (i = 1; i < size; i++) {
        key = arr[i];
        j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}


void printArr(int arr[], int size) {
    int i;
    for (i = 0; i < size; i++)
        printf("%d ", arr[i]);
    printf("\n");
}

int main() {
    int arr1[] = { 9,8,1,3,2 };
    int arr2[] = { 9,8,1,3,2 };
    int arr3[] = { 9,8,1,3,2 };
    int size = 5;
    printArr(arr1, size);
    selectionSort(arr1, size);
    printArr(arr1, size);
    printf("\n");

    printArr(arr2, size);
    bubbleSort(arr2, size);
    printArr(arr2, size);
    printf("\n");

    printArr(arr3, size);
    insertionSort(arr3, size);
    printArr(arr3, size);

    return 0;
}

 

정렬 알고리즘에는 이 밖에도 병합정렬, 퀵정렬, 힙정렬 등이 있습니다. 일단 이것부터숙지하도록 합시다.

 

반응형
블로그 이미지

REAKWON

와나진짜

,

트리

트리는 비선형 자료구조로 노드로 구성되어 있습니다. 노드들이 링크(선)를 통해 연결되어 있으며 계층적 구조를 나타내고자 할때 사용됩니다.


아래의 트리 그림을 가지고 하나하나 기본적인 설명을 하도록 하겠습니다.






⊙루트노드 (root) : 트리의 가장 위, 즉 최상위 노드를 말합니다. 트리에서 가장 중요한 노드입니다. 여기서 A가 루트노드입니다.




잎 또는 단말 노드 (leaf) : 트리의 가장 끝자락에 위치한 노드입니다. 위 트리에서는 G, H, E, I, J가 해당됩니다.


부모노드 (parent) : 현재노드의 한단계 위 계층에 있는 노드를 말합니다. I의 부모노드는 F, C의 부모노드는 A입니다. 


형제노드 (sibling) : 부모가 같은 노드를 의미합니다. D의 형제노드는 E, G의 형제노드는 H입니다.


내부노드 (internal) : leaf가 아닌 노드를 의미합니다.

노드 깊이 (depth) : 루트에서 시작해서 얼마나 깊이 들어가야 그 노드를 만날 수 있는지를 깊이라고 합니다. 기준이 루트노드이므로 A의 깊이는 0, C의 깊이는 1, H의 깊이는 3입니다.

노드 레벨 (level) : 같은 깊이를 갖는 노드의 집합입니다. 단, 깊이와는 다르게 1부터 시작합니다. 레벨 1의 노드는 A, 레벨 2의 노드들은 B, C, 레벨 3의 노드들은 D, E, F가 되겠죠.

트리 높이 (height) : 가장 긴 깊이를 의미합니다. 위 트리의 높이는 3이 됩니다. 트리 높이는 트리에서 가장 중요한 요소인데, 노드의 갯수만큼 높이가 커지면 linkedlist와 성능이 같게 되죠. 그래서 높이가 트리에서 성능을 좌우합니다.

노드 차수 (degree) : 각 노드에서 나가는 선을 의미합니다. 즉, 가지수를 말하겠죠? A의 차수는 2, C의 차수는 1이 됩니다.

트리의 차수 :  가장 큰 차수를 갖고 있는 노드의 차수가 됩니다. 위의 트리에서는 2보다 큰 차수는 없으니, 트리의 차수는 2가 됩니다.

트리의 노드가 N이라고 할때 가지수는 무조건 N-1이 됩니다. 왜냐면 트리에서는 사이클이 존재하지 않기 때문인데, 위 트리에서 노드와 노드사이에 선을 한개만 더 추가하게 되면 사이클이 발생합니다. 특정 노드와 노드사이를 계속 돌 수 있다는 이야기죠. 그러니, 사이클이 발생하는 트리는 없습니다.


트리 순회


트리에는 크게 세가지의 순회 방법이 있습니다. 그 노드를 어떤 순서로 방문하느냐에 따라서 달라집니다. 그 전에 트리를 구현하는 방법을 알아보아야겠죠?


노드가 어떤 식으로 구성되어 있는 지 잠깐만 살펴보겠습니다.

typedef struct node {
	char data;
	struct node *left;
	struct node *right;
} Node;



각 노드는 data라는 변수를 갖고 있습니다. 이 data의 변수는 위 그림에서 알파벳을 저정하고 있습니다.

그리고 left는 그 노드의 왼쪽 자식 노드, right는 그 노드의 오른쪽 자식노드를 의미합니다. 가장 끝에 있는 노드는 left와 right가 NULL이라는 점입니다.


위 트리를 3가지 순회방법인 전위순회, 후위순회, 중위순회 순으로 탐색해봅시다




전위순회(preorder traversal)

전위순회는 현재 노드를 가장 먼저 방문합니다. 그러니 왼쪽 자식과 오른쪽 자식을 현재 자식보다 나중에 방문합니다.

위 그래프를 전위순회로 방문하게 된다면 

A -> B -> D -> G -> H -> E -> C ->F ->I -> J

순으로 트리가 방문됩니다.

void preorder(Node *node) {
	if (node == NULL) return;
	printf(" %c ", node->data);
	preorder(node->left);
	preorder(node->right);
}



후위순회(postorder traversal)

이름에서부터 짐작하셨겠지만 현재 노드를 가장 나중에 방문하는 방식의 순회입니다. 그러니 왼쪽 자식, 오른쪽 자식 노드를 우선적으로 방문합니다.

그러니 후위순회의 결과는

G -> H -> D -> E -> B ->I -> J -> F -> C -> A

입니다.

void postorder(Node *node) {
	if (node == NULL) return;
	postorder(node->left);
	postorder(node->right);
	printf(" %c ", node->data);
}




중위순회(inorder traversal)

우선 왼쪽 자식을 방문하고, 현재 노드를 방문합니다. 그리고 나서 오른쪽 자식 노드를 방문하게 됩니다. 현재 노드를 중간에 방문한다고 해서 중위순회가 됩니다.

방문 순서는 이렇습니다.

G -> D -> H -> B -> E -> A -> C ->I -> F -> J


void inorder(Node *node) {
	if (node == NULL) return;
	inorder(node->left);
	printf(" %c ", node->data);
	inorder(node->right);
}




전체코드

전체코드는 아래와 같습니다. 위의 트리를 그대로 코드로 옮긴거죠. 입력받으면서 동적으로 할 수도 있겠지만 그런 귀찮은 부분까지는 고려하지 않았습니다. 여러분들이 더 잘하실테니까요.

#include <stdio.h>
#include <stdlib.h>
typedef struct node {
	char data;
	struct node *left;
	struct node *right;
} Node;

Node *createNode(char data) {
	Node *node = (Node*)malloc(sizeof(Node));
	node->data = data;
	node->left = NULL;
	node->right = NULL;
	return node;
}
void preorder(Node *node) {
	if (node == NULL) return;
	printf(" %c ", node->data);
	preorder(node->left);
	preorder(node->right);
}

void postorder(Node *node) {
	if (node == NULL) return;
	postorder(node->left);
	postorder(node->right);
	printf(" %c ", node->data);
}

void inorder(Node *node) {
	if (node == NULL) return;
	inorder(node->left);
	printf(" %c ", node->data);
	inorder(node->right);
}


int main() {
	Node *A = createNode('A');
	Node *B = createNode('B');
	Node *C = createNode('C');
	Node *D = createNode('D');
	Node *E = createNode('E');
	Node *F = createNode('F');
	Node *G = createNode('G');
	Node *H = createNode('H');
	Node *I = createNode('I');
	Node *J = createNode('J');
	A->left = B;
	A->right = C;
	B->left = D;
	B->right = E;
	D->left = G;
	D->right = H;
	C->right = F;
	F->left = I;
	F->right = J;


	printf("전위 순회: ");
	preorder(A);
	printf("\n");

	printf("후위 순회: ");
	postorder(A);
	printf("\n");

	printf("중위 순회: ");
	inorder(A);
	printf("\n");

	return 0;
}


반응형
블로그 이미지

REAKWON

와나진짜

,

파일 입출력 2


간단하게 파일에서 읽고 쓰는 것은 이제 알겠습니다.


하지만 단순히 파일을 처음부터 차례대로 읽는 것이 아니라, 어떤 위치 이후에서 읽고 쓰는 것도 가능할까요?


할 수 있습니다. 바로 fseek함수를 이용해서 말이죠.


fseek

int fseek(FILE *stream, long int offset, int origin);


fseek함수는 파일의 위치를 임의로 조작할 수 있습니다. 파일은 순차적으로 읽고 씁니다. 하지만 특정위치에 있는 데이터를 읽거나 쓸 필요가 있는 것이죠.


stream은 파일을 의미합니다.

offset은 origin에서 얼마만큼 이동할 것인지 나타냅니다.

origin은 기준 위치를 지정합니다.




우리가 fread를 여러번 호출해도 순차적으로 읽어올 수 있는 이유는 파일포인터때문입니다. 파일포인터는 현재 파일의 위치를 기억하고 있습니다. 그렇기 때문에 순차적으로 읽을 수 있는 거지요.






우리는 파일포인터의 위치를 임의로 지정하여 특정 위치의 데이터를 읽고 쓰는 것이 가능합니다. 파일포인터에 관해서는 세가지 매크로가 있는데요.


SEEK_SET : 파일의 처음

SEEK_CUR : 지금 현재 파일포인터 위치

SEEK_END : 파일의 끝


특정 위치에서 읽기

특정 위치에 값을 읽어오는 코드를 한 번 보겠습니다.


text.txt

ABCDEFGHIJ


#include <stdio.h> #include <string.h> int main() { FILE *fp = fopen("text.txt", "r"); char buf[30]; memset(buf, 0, sizeof(buf)); fseek(fp, 5L, SEEK_SET); // fseek(fp, -4L, SEEK_END); fread(buf, sizeof(char), sizeof(buf), fp); printf("%s\n", buf); fclose(fp); return 0; }


파일의 처음위치부터 5를 건너뛰면 파일포인터는 F 앞을 가리키고 있습니다.


결과

FGHIJ



반면에 파일 끝에서 4칸 앞은 offset이 음수가 됩니다. 처음 fseek을 주석처리하고 밑의 주석을 해제하고 실행해보세요. 결과는 이렇습니다.


GHIJ



특정 위치에 쓰기

반면 특정 위치에 쓰는 것도 가능합니다. 단! 파일을 열때 r+모드로 여세요. 


text.txt

ABCDEFGHIJ



#include <stdio.h>
#include <string.h>
int main() {
	FILE *fp = fopen("text.txt", "r+");
	char buf[30]="_INSERT_";

	fseek(fp, -5, SEEK_END);
	fwrite(buf, sizeof(char),strlen(buf), fp);
	
	fclose(fp);
	return 0;
}


파일 끝에서 5칸 앞에 _INSERT_라는 문자열을 추가하는 코드입니다. F 앞의 위치입니다. 

하지만 그 자리를 덮어쓰게 됩니다. 그래서 결과는 이렇습니다.


text.txt

ABCDE_INSERT_




ftell

파일 포인터의 위치를 알아옵니다. 

long int ftell(FILE *stream)


열었던 파일을 매개변수로 넣어주면 끝입니다.


성공시 0을 포함한 양수를 반환합니다.


text.txt

ABCDEFGHIJ


#include <stdio.h>

int main() {
	FILE *fp = fopen("text.txt", "r+");
	int pos;
	fseek(fp, 5, SEEK_SET);
	pos = ftell(fp);
	printf("현재 파일 포인터 : %d\n", pos);
	fclose(fp);
	return 0;
}


결과

현재 파일 포인터 : 5


ftell을 응용해서 파일의 크기도 알아올 수 있습니다.

#include <stdio.h>

int main() {
	FILE *fp = fopen("text.txt", "r+");
	int pos;
	fseek(fp, 0, SEEK_END);
	pos = ftell(fp);
	printf("파일크기 : %d\n", pos);
	fclose(fp);
	return 0;
}


파일 속성에서 알아본 크기는 10바이트입니다. 위의 코드의 결과는 어떨까요?


파일크기 : 10


바이트 단위로 일치한다는 것을 알 수 있네요.


파일에 대해서는 다음에 또 이야기하도록 합시다.

반응형
블로그 이미지

REAKWON

와나진짜

,