'자료구조 알고리즘'에 해당되는 글 20건

  1. 2020.12.19 해시
  2. 2020.12.18 AVL 트리
  3. 2020.12.18 이진 탐색 트리
  4. 2020.12.18 너비 우선 탐색
  5. 2020.12.17 깊이 우선 탐색
  6. 2020.12.17 그래프의 개념과 구현
  7. 2020.12.17 순차 탐색과 이진 탐색
  8. 2020.12.17 우선순위 큐
  9. 2020.12.17 이진 트리의 구현 및 순회
  10. 2020.12.17 이진 트리의 개념

해시

자료구조 알고리즘 2020. 12. 19. 15:20

해시(Hash)

- 데이터를 최대한 빠른 속도로 관리하도록 도와주는 자료 구조

- 해시를 사용하면 메모리 공간이 많이 소모되지만 매우 빠른 속도로 데이터를 처리할 수 있음

- 빠르게 데이터에 접근할 수 있다는 점에서 데이터베이스 등의 소프트웨어에서 활용

 

해시 함수 동작 과정

- 특정한 값을 찾고자 할 때는 그 값의 키로 접근할 수 있음

- 일반적으로 해시 함수는 모듈로 연산 등의 수학적 연산으로 이루어져 있으므로 O(1)만에 값에 접근할 수 있음

 

* 특정한 값이 들어왔을 때 그 값에 10을 나눈 나머지 값이 키가 될 수 있도록 했다고 가정

입력 데이터 >  해시 함수(n mod 10)  > 키(Key) | 값(Value)

78 / khon                                              2           | khon01

12 / khon01                                          4           | khon03

55 / khon02                                         5           | khon02

64 / khon03                                         8           | khon

 

해시의 충돌

- 해시 함수의 입력 값으로는 어떠한 값이나 모두 들어갈 수 있지만 해시 함수를 거쳐 생성되는 키의 경우의 수는 한정적이기 때문에 키 중복이 발생할 수 있음

- 해시 테이블에서 키가 중복되는 경우 충돌(Collision)이 발생했다고 표현

- 동일한 데이터 값이 들어갈 경우 충돌

 

생일의 역설

- 무작위의 사람 57명을 한 곳에 모아 놓으면, 2명의 생일이 같을 확률이 99%

- 이를 해싱 과정과 비교하자면 '365로 나눈 나머지'를 키로 이용하는 해시 함수의 경우 입력 데이터가 57개만 되어도 사실상 충돌이 무조건 발생한다고 판단해야 하는 것

 

해싱(Hashing)

- 해싱 알고리즘은 나눗셈 법이 가장 많이 활용됨

- 입력 값을 테이블의 크기로 나눈 나머지를 키로 이용하는 방식

- 테이블의 크기는 소수로 설정하는 것이 효율성이 높음

 

충돌을 처리하는 방법

- 아무리 잘 만들어진 해시 함수라고 해도 충돌이 발생할 수밖에 없음

- 충돌을 처리하는 방법은 다음과 같이 두 가지 유형으로 분류할 수 있음

1. 충돌을 발생시키는 항목을 해시 테이블의 다른 위치에 저장 : 선형 조사법, 이차 조사법 등

2. 해시 테이블의 하나의 버킷에 여러 개의 항목을 저장 : 체이닝 등

 

선형 조사법

- 데이터가 충돌할 경우 그다음 데이터 비어있다면 그곳에 데이터를 넣음

// 선형 조사법 구조체 정의
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define TABLE_SIZE 10007
#define INPUT_SIZE 5000

typedef struct {
    int id;
    char name[20];
} Student;

// 해시 테이블 초기화
void init(Student** hashTable) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        hashTable[i] = NULL;
    }
}

// 해시 테이블 메모리 반환
void destructor(Student** hashTable) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        if (hashTable[i] != NULL) {
            free(hashTable[i]);
        }
    }
}

// 해시 테이블 내 빈 공간을 선형 탐색으로 찾음
int findEmpty(Student** hashTable, int id) {
    int i = id % TABLE_SIZE;
    while (1) {
        if (hashTable[i % TABLE_SIZE] == NULL) {
            return i % TABLE_SIZE;
        }
        i++;
    }
}

// 특정한 ID 값에 매칭되는 데이터를 해시 테이블 내에서 찾음
int searh(Student** hashTable, int id) {
    int i = id % TABLE_SIZE;
    while (1) {
        if (hashTable[i % TABLE_SIZE] == NULL) return -1;
        else if (hashTable[i % TABLE_SIZE]->id == id) return i;
        i++;
    }
}

// 특정한 키 인덱스에 데이터를 삽입
void add(Student** hashTable, Student* input, int key) {
    hashTable[key] = input;
}

// 해시 테이블에서 특정한 키의 데이터를 반환
Student* getValue(Student** hashTable, int key) {
    return hashTable[key];
}

// 선형 조사법 데이터 출력 함수
// 해시 테이블에 존재하는 모든 데이터를 출력
void show(Student** hashTable) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        if (hashTable[i] != NULL) {
            printf("키: [%d] 이름: [%s]\n", i, hashTable[i]->name);
        }
    }
}

int main(void) {
    Student **hashTable;
    hashTable = (Student**)malloc(sizeof(Student) * TABLE_SIZE);
    init(hashTable);
    
    for (int i = 0; i < INPUT_SIZE; i++) {
        Student* student = (Student*)malloc(sizeof(Student));
        student->id = rand() % 1000000;
        sprintf(student->name, "사람%d", student->id);
        if (searh(hashTable, student->id) == -1) { // 중복되는 ID는 존재하지 않도록 함
            int index = findEmpty(hashTable, student->id);
            add(hashTable, student, index);
        }
    }
    
    show(hashTable);
    destructor(hashTable);
    system("pause");
    return 0;
}

 

선형 조사법의 단점

- 충돌이 발생하기 시작하면 유사한 값을 가지는 데이터끼리 서로 밀집되는 집중 결함 문제가 존재

- 메모리를 많이 소모한다면 충돌이 적게 발생하므로 매우 바른 데이터 접근 속도를 가질 수 있음

- 일반적인 경우 해시 테이블의 크기는 한정적이므로 충돌이 최대한 적게 발생하도록 해야 함

 

이차 조사법

- 선형 조사법을 약간 변형한 형태로 키 값을 선택할 때 완전 제곱수를 더해 나가는 방식

- 데이터를 입력하다가 충돌이 발생할 경우 1만큼 떨어진 공간에서 확인을 하고 그 자리에 숫자가 차 있다면 2의 제곱만큼 넘어서 자리를 확인 

- 기존에 발생했던 데이터가 밀집되는 현상이 줄어들기 때문에 해시 충돌이 적게 발생한다는 특징이 있음

 

조사법의 테이블 크기 재설정

- 일반적으로 선형 조사법, 이차 조사법 등의 조사법에서 테이블이 가득 차게 되면 테이블의 크기를 확장하여 계속해서 해시 테이블을 유지할 수 있도록 함

 

체이닝(Chaining)

- 연결 리스트를 활용해 특정한 키를 가지는 항목들을 연결하여 저장함

- 연결 리스트를 사용한다는 점에서 삽입과 삭제가 용이함

- 테이블 크기 재설정 문제는 동적 할당을 통해 해결할 수 있음

- 동적 할당을 위한 추가적인 메모리 공간이 소모된다는 단점이 있음

- 충돌이 발생하더라도 무시하고 키 값에 해당하는 연결 리스트에 계속해서 데이터를 넣음

// 체이닝 구조체 정의
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define TABLE_SIZE 10007
#define INPUT_SIZE 5000

typedef struct {
    int id;
    char name[20];
} Student;


typedef struct {
    Student* data;
    struct Bucket* next;
} Bucket;


// 해시 테이블 초기화
void init(Bucket** hashTable) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        hashTable[i] = NULL;
    }
}

// 해시 테이블 메모리 반환
void destructor(Bucket** hashTable) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        if (hashTable[i] != NULL) {
            free(hashTable[i]);
        }
    }
}

// 체이닝 데이터 탐색 함수
int isExist(Bucket** hashTable, int id) {
    int i = id % TABLE_SIZE;
    if (hashTable[i] == NULL) return 0;
    else {
        Bucket *cur = hashTable[i];
        while (cur != NULL) {
            if (cur->data->id == id) return 1;
            cur = cur->next;
        }
    }
    return 0;
}

// 체이닝 데이터 삽입 함수
// 특정한 키 인덱스에 데이터를 삽입
void add(Bucket** hashTable, Student* input) {
    int i = input->id % TABLE_SIZE;
    if (hashTable[i] == NULL) {
        hashTable[i] = (Bucket*)malloc(sizeof(Bucket));
        hashTable[i]->data = input;
        hashTable[i]->next = NULL;
    }
    else {
        Bucket *cur = (Bucket*)malloc(sizeof(Bucket));
        cur->data = input;
        cur->next = hashTable[i];
        hashTable[i] = cur;
    }
}

// 체이닝 데이터 출력 함수
// 해시 테이블에 존재하는 모든 데이터를 출력
void show(Bucket** hashTable) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        if (hashTable[i] != NULL) {
            Bucket *cur = hashTable[i];
            while (cur != NULL) {
                printf("키: [%d] 이름: [%s]\n", i, cur->data->name);
                cur = cur->next;
            }
        }
    }
}

int main(void) {
    Bucket **hashTable;
    hashTable = (Bucket**)malloc(sizeof(Bucket) * TABLE_SIZE);
    init(hashTable);
    
    for (int i = 0; i < INPUT_SIZE; i++) {
        Student* student = (Student*)malloc(sizeof(Student));
        student->id = rand() % 1000000;
        sprintf(student->name, "사람%d", student->id);
        if (!isExist(hashTable, student->id)) { // 중복되는 ID는 존재하지 않도록 함
            add(hashTable, student);
        }
    }
    show(hashTable);
    destructor(hashTable);
    system("pause");
    return 0;
}

 

'자료구조 알고리즘' 카테고리의 다른 글

AVL 트리  (0) 2020.12.18
이진 탐색 트리  (0) 2020.12.18
너비 우선 탐색  (0) 2020.12.18
깊이 우선 탐색  (0) 2020.12.17
그래프의 개념과 구현  (0) 2020.12.17
Posted by khon98
,

AVL 트리

- AVL 트리는 균형이 갖춰진 이진 트리를 의미

- 완전 이진 트리는 검색에 있어서 O(logN)의 시간 복잡도를 유지할 수 있음

- AVL 트리는 간단한 구현 과정으로 특정 이진 트리가 완전 이진 트리에 가까운 형태를 유지하도록 해줌

- AVL 트리는 균형 인수라는 개념을 이용

- 모든 노드에 대한 균형 인수가 +1, 0, -1인 트리를 의미

- 균형 인수가 위 세가지 경우에 해당하지 않는 경우 회전을 통해 트리를 재구성해야 함

 

균형 인수 = | 왼쪽 자식 높이 - 오른쪽 자식 높이 |

 

AVL 트리의 회전

- AVL 트리는 총 4가지 형식에 의하여 균형이 깨질수 있음

- 균형 인수가 깨지는 노드를 X라고 했을때 4가지 형식은 다음과 같음

1. LL형식 : X의 왼쪽 자식의 왼쪽에 삽입하는 경우

2. LR형식 : X의 왼쪽 자식의 오른쪽에 삽입하는 경우

3. RR형식 : X의 오른쪽 자식의 오른쪽에 삽입하는 경우

4. RL형식 : X의 오른족 자식의 왼쪽에 삽입하는 경우

 

AVL 트리의 높이

- 각 노드는 균형 인수를 계산하기 위한 목적으로 자신의 높이 값을 가짐

 

AVL 트리의 균형 잡기

- 각 노드가 삽입 될 때마다 수행되어야 함

- 삽입 과정에 소요되는 시간 복잡도는 O(logN)

- 각 트리의 균형 잡기는 삽입 시에 거치는 모든 노드에 대하여 수행된다는 점에서 O(1)의 시간 복잡도를 만족해야 함

 

AVL 트리의 삽입

- 일반적인 이진 탐색 트리와 흡사함

- 다만 삽입 시에 거치는 모든 노드에 대하여 균형 잡기를 수행해주어야 한다는 특징이 있음

 

AVL 트리의 출력 함수

- 삽입되는 과정을 면밀히 살펴보는 것이 중요하므로 트리 구조를 적절히 보여 줄 수 있는 방식으로 출력해야 함

- 일반적으로 트리의 너비가 높이보다 크다는 점에서 세로 방향으로 출력하는 함수를 구현할 수 있음

 

#include <stdio.h>
#include <stdlib.h>

// AVL 트리의 정의
int getMax(int a, int b) {
    if (a > b) return a;
    return b;
}

typedef struct {
    int data;
    int height; // 높이를 저장해야 시간 복잡도를 보장할 수 있음
    struct Node* leftChild;
    struct Node* rightChild;
} Node;

// AVL 트리의 높이 계산 함수
int getHeight(Node* node) {
    if (node == NULL) return 0;
    return node->height;
}

// 모든 노드는 회전을 수행한 이후에 높이를 다시 계산
void setHeight(Node* node) {
    node->height = getMax(getHeight(node->leftChild), getHeight(node->rightChild)) + 1;
}

int getDifference(Node* node) {
    if (node == NULL) return 0;
    Node* leftChild = node->leftChild;
    Node* rightChuld = node->rightChild;
    return getHeight(leftChild) - getHeight(rightChuld);
}

// AVL 트리 LL회전
Node* rotateLL(Node* node) {
    Node *leftChild = node->leftChild;
    node->leftChild = leftChild->rightChild;
    leftChild->rightChild = node;
    setHeight(node); // 회전 이후에 높이를 다시 계산
    return leftChild; // leftChild는 이후에 setHeight() 수행
}

// AVL 트리 RR회전 (LL회전을 반대로 수행)
Node* rotateRR(Node* node) {
    Node *rightChild = node ->rightChild;
    node->rightChild = rightChild->leftChild;
    rightChild->leftChild = node;
    setHeight(node);
    return rightChild;
}

// AVL 트리의 LR회전
Node *rotateLR(Node* node) {
    Node *leftChild = node->leftChild;
    node->leftChild = rotateRR(leftChild);
    setHeight(node->leftChild); // 회전 이후에 높이를 다시 계산
    return rotateLL(node); // 해당 노드는 이후에 setHeight() 수행
}

// AVL 트리의 RL회전
Node *rotatePR(Node* node) {
    Node *rightChild = node->rightChild;
    node->rightChild = rotateLL(rightChild);
    setHeight(node->rightChild);
    return rotateRR(node);
}

// AVL 트리의 균형 잡기
Node* balance(Node* node) {
    int difference = getDifference(node);
    if (difference >= 2) {
        if (getDifference(node->leftChild) >= 1) {
            node = rotateLL(node);
        }
        else {
            node = rotateLR(node);
        }
    }
    else if (difference <= -2) {
        if (getDifference(node->rightChild) <= -1) {
            node = rotateRR(node);
        }
        else {
            node = rotateRR(node);
        }
    }
    setHeight(node); // 회전 이후에 높이를 다시 계산
    return node;
}

// AVL 트리의 삽입
Node *insertNode(Node* node, int data) {
    if (!node) {
        node = (Node*)malloc(sizeof(Node));
        node->data = data;
        node->height = 1;
        node->leftChild = node->rightChild = NULL;
    }
    else if (data < node->data) {
        node->leftChild = insertNode(node->leftChild, data);
        node = balance(node);
    }
    else if (data > node->data) {
        node->rightChild = insertNode(node->rightChild, data);
        node = balance(node);
    }
    else {
        printf("데이터 중복 발생\n");
    }
    return node;
}

// AVL 트리의 출력 함수
Node* root = NULL;

void display(Node* node, int level) {
    if (node != NULL) {
        display(node->rightChild, level + 1);
        printf("\n");
        if (node == root) {
            printf("Root: ");
        }
        for (int i = 0; i < level && node != root; i++) {
            printf("    ");
        }
        printf("%d(%d)", node->data, getHeight(node));
        display(node->leftChild, level + 1);
    }
}

// AVL 트리
int main(void) {
    root = insertNode(root, 7);
    root = insertNode(root, 6);
    root = insertNode(root, 5);
    root = insertNode(root, 3);
    root = insertNode(root, 1);
    root = insertNode(root, 9);
    root = insertNode(root, 8);
    root = insertNode(root, 12);
    root = insertNode(root, 16);
    root = insertNode(root, 18);
    root = insertNode(root, 23);
    root = insertNode(root, 21);
    root = insertNode(root, 14);
    root = insertNode(root, 15);
    root = insertNode(root, 19);
    root = insertNode(root, 20);
    display(root, 1); printf("\n");
    system("pause");
}

'자료구조 알고리즘' 카테고리의 다른 글

해시  (0) 2020.12.19
이진 탐색 트리  (0) 2020.12.18
너비 우선 탐색  (0) 2020.12.18
깊이 우선 탐색  (0) 2020.12.17
그래프의 개념과 구현  (0) 2020.12.17
Posted by khon98
,

이진 탐색 트리(Binary Search Tree)

- 항상 동작하도록 구현하여 탐색 속도를 극대화시킨 자료구조를 이진 탐색 트리라고 함

- 항상 부모 노드가 왼쪽 자식보다 크고 오른쪽 자식보다는 작음

 

이진 탐색 트리의 탐색

- 한 번 확인할 때마다 보아야 하는 원소의 개수가 절반씩 줄어든다는 점에서 완전 이진트리인 경우 탐색 시간의 O(logN)의 시간 복잡도를 가짐

- 이진 탐색 트리에서 탐색을 할 때는 찾고자 하는 값이 부모 노드보다 작을 경우, 왼쪽 자식으로 찾고자 하는 값이 부모 노드보다 클 경우, 오른쪽 자식으로 이어 나가면서 방문

 

이진 탐색 트리의 삭제

1. 자식이 없는 경우

- 삭제할 노드의 자식이 없는 경우 단순히 제거하면 됨

 

2. 자식이 하나만 존재하는 경우

- 삭제할 노드의 자식이 하나만 존재하는 경우 삭제할 노드의 자리에 자식 노드를 넣음

 

3. 자식이 둘 다 존재하는 경우

- 자식이 둘 다 존재하는 경우 그다음으로 삭제할 노드의 자리에 자기 다음으로 큰 노드를 넣음

 

// 이진 탐색 트리의 정의
#include <stdio.h>
#include <stdlib.h>
    
typedef struct {
    int data;
    struct Node* leftChild;
    struct Node* rightChild;
} Node;

// 이진 탐색 트리의 삽입
Node* insertNode(Node* root, int data) {
    if (root == NULL) {
        root = (Node*) malloc(sizeof(Node));
        root->leftChild = root ->rightChild = NULL;
        root->data = data;
        return root;
    }
    else {
        if (root->data > data) {
            root->leftChild = insertNode(root->leftChild, data);
        }
        else {
            root->rightChild = insertNode(root->rightChild, data);
        }
    }
    return root;
}

// 이진 탐색 트리의 탐색
Node* searchNode(Node* root, int data) {
    if (root == NULL) return NULL;
    if (root->data == data) return root;
    else if (root->data > data) return searchNode(root->leftChild, data);
    else return searchNode(root->rightChild, data);
}

// 이진 탐색 트리의 순회
void prorder(Node* root) {
    if (root == NULL) return;
    printf("%d ", root->data);
    prorder(root->leftChild);
    prorder(root->rightChild);
}

// 이진 탐색 트리의 가장 작은 원소 찾기
Node* findMinNode(Node* root) {
    Node* node = root;
    while (node->leftChild != NULL) {
        node = node->leftChild;
    }
    return node;
}

// 이진 탐색 트리의 삭제 함수
Node* deleteNode(Node* root, int data) {
    Node* node = NULL;
    if (root == NULL) return NULL;
    if (root->data > data) root->leftChild = deleteNode(root->leftChild, data);
    else if (root->data < data) root->rightChild = deleteNode(root->rightChild, data);
    else {
        if (root->leftChild != NULL && root->rightChild != NULL) {
            node = findMinNode(root->rightChild);
            root->data = node->data;
            root->rightChild = deleteNode(root->rightChild, node->data);
        }
        else {
            node = (root->leftChild != NULL) ? root->leftChild : root->rightChild;
            free(root);
            return node;
        }
    }
    return root;
}

// 이진 탐색 트리 사용
int main(void) {
    Node* root = NULL;
    root = insertNode(root, 30);
    root = insertNode(root, 17);
    root = insertNode(root, 48);
    root = insertNode(root, 5);
    root = insertNode(root, 23);
    root = insertNode(root, 37);
    root = insertNode(root, 50);
    root = insertNode(root, 30);
    prorder(root);
    system("pause");
}

* redefinition of 'preorder' as different kind of symbol - preorder를 다른 이름으로 재정의

 

완전 이진트리(Complete Binary Tree)

- 이진 탐색 트리의 성능을 최대로 끌어내기 위해서는 이진 탐색 트리가 완전 이진트리에 가까워질 수 있도록 설계해야 함

- 루트 노드부터 시작해서 자식 노드가 왼쪽 자식 노드 오른쪽 자식 노드로 차례대로 들어가는 구조의 이진트리

 

트리의 효율성

- 트리를 사용하면 데이터를 처리함에 있어서 효율적

- 트리에서 데이터의 개수가 N개일 때 배열과 마찬가지로 O(N)의 공간만이 소요되며 삽입 및 삭제에 있어서 일반적인 경우 기존의 배열을 이용하는 방식보다 효율적

- 데이터 베이스 등 대용량 저장 및 검색 자료구조로 많이 활용됨

- 반면에 한쪽으로 치우친 편향 이진트리의 경우 탐색에 있어 O(N)의 시간 복잡도가 형성되므로 기존의 배열을 사용하는 것보다 오히려 많은 공간과 시간이 낭비됨

- 이진트리를 만들 때는 트리의 균형이 맞도록 설정하는 것이 중요

'자료구조 알고리즘' 카테고리의 다른 글

해시  (0) 2020.12.19
AVL 트리  (0) 2020.12.18
너비 우선 탐색  (0) 2020.12.18
깊이 우선 탐색  (0) 2020.12.17
그래프의 개념과 구현  (0) 2020.12.17
Posted by khon98
,

너비 우선 탐색(Breadth First Search)

- 너비를 우선으로 하여 탐색을 수행하는 탐색 알고리즘

- DFS와 마찬가지로 맹목적으로 전체 노드를 탐색하고자 할 때 자주 사용되며 큐 자료구조에 기초함

- 너비 우선 탐색은 고급 그래프 탐색 알고리즘에서 자주 활용되므로 고급 개발자가 되기 위해서는 너비 우선 탐색에 대해 숙지해야 함

- 탐색 시작 노드를 큐에 삽입하고 방문 처리를 함

- 큐에서 노드를 꺼내 해당 노드의 인접 노드 중에서 방문하지 않은 노드들을 모두 큐에 삽입하고 방문 처리를 함

- 위에 과정을 더 이상 수행할 수 없을 때까지 반복

 

연결 리스트 정의

// 연결 리스트 정의
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define INF 99999999
#define MAX_SIZE 1001

typedef struct {
    int index;
    struct Node *next;
} Node;

typedef struct {
    Node *front;
    Node *rear;
    int count;
} Queue;

Node** a;
int n, m, c[MAX_SIZE];

// 연결 리스트 삽입 함수
void addFront(Node *root, int index) {
    Node *node = (Node*)malloc(sizeof(Node));
    node->index = index;
    node->next = root->next;
    root->next = node;
}

// 큐 삽입 함수
void queuePush(Queue *queue, int index) {
    Node *node = (Node*)malloc(sizeof(Node));
    node->index = index;
    node->next = NULL;
    if (queue->count == 0) {
        queue->front = node;
    }
    else {
        queue->rear->next = node;
    }
    queue->rear = node;
    queue->count++;
}

// 큐 추출 함수
int queuePop(Queue *queue) {
    if (queue->count == 0) {
        printf("큐 언더플로우 발생\n");
        return -INF;
    }
    Node *node = queue->front;
    int index = node->index;
    queue->front = node->next;
    free(node);
    queue->count--;
    return index;
}

// 너비 우선 탐색 함수
void bfs(int start) {
    Queue q;
    q.front = q.rear = NULL;
    q.count = 0;
    queuePush(&q, start);
    c[start] = 1;
    while (q.count != 0) {
        int x = queuePop(&q);
        printf("%d ", x);
        Node *cur = a[x]->next;
        while (cur != NULL) {
            int next = cur->index;
            if (!c[next]) {
                queuePush(&q, next);
                c[next] = 1;
            }
            cur = cur->next;
        }
    }
}

// 너비 우선 탐색 이용해보기
int main(void) {
    scanf("%d %d", &n, &m);
    a = (Node**)malloc(sizeof(Node*) * (MAX_SIZE));
    for (int i = 1; i <= n; i++) {
        a[i] = (Node*)malloc(sizeof(Node));
        a[i]->next = NULL;
    }
    for (int i = 0; i < m; i++) {
        int x, y;
        scanf("%d %d", &x, &y);
        addFront(a[x], y);
        addFront(a[y], x);
    }
    bfs(1);
    system("pause");
    return 0;
}

'자료구조 알고리즘' 카테고리의 다른 글

AVL 트리  (0) 2020.12.18
이진 탐색 트리  (0) 2020.12.18
깊이 우선 탐색  (0) 2020.12.17
그래프의 개념과 구현  (0) 2020.12.17
순차 탐색과 이진 탐색  (0) 2020.12.17
Posted by khon98
,

깊이 우선 탐색(Depth First Search)

- 탐색을 함에 있어서 보다 깊은 것을 우선적으로 하여 탐색하는 알고리즘

- 깊이 우선 탐색은 기본적으로 전체 노드를 맹목적으로 탐색하고자 할 때 사용

- 깊이 우선 탐색 알고리즘은 스택 자료구조에 기초함

- 깊이 우선 탐색은 빠르게 모든 경우의 수를 탐색하고자 할 때 쉽게 사용할 수 있음

- 탐색 시작 노드를 스택에 삽입하고 방문 처리를 함

- 스택의 최상단 노드에게 방문하지 않은 인접 노드가 하나라도 있으면 그 노드를 스택에 넣고 방문 처리를 함

- 방문하지 않은 인접 노드가 없으면 스택에서 최상단 노드를 꺼냄

- 위에 과정을 더 이상 수행할 수 없을 때까지 반복

 

연결 리스트 정의

// 연결 리스트 정의
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 1001

typedef struct {
    int index;
    struct Node *next;
} Node;

Node** a;
int n, m, c[MAX_SIZE];

// 연결 리스트 삽입 함수
void addFront(Node *root, int index) {
    Node *node = (Node*)malloc(sizeof(Node));
    node->index = index;
    node->next = root->next;
    root->next = node;
}

// 깊이 우선 탐색 함수
void dfs(int x) {
    if (c[x]) return;
    c[x] = 1;
    printf("%d ", x);
    Node *cur = a[x]->next;
    while (cur != NULL) {
        int next = cur->index;
        dfs(next);
        cur = cur->next;
    }
}

// 깊이 우선 탐색 이용
int main(void) {
    scanf("%d %d", &n, &m);
    a = (Node**) malloc(sizeof(Node*) * (MAX_SIZE));
    for (int i = 1; i <= n; i++) {
        a[i] = (Node*)malloc(sizeof(Node));
        a[i]->next = NULL;
    }
    for (int i = 0; i < m; i++) {
        int x, y;
        scanf("%d %d", &x, &y);
        addFront(a[x], y);
        addFront(a[y], x);
    }
    dfs(1);
    system("pause");
    return 0;
}

'자료구조 알고리즘' 카테고리의 다른 글

이진 탐색 트리  (0) 2020.12.18
너비 우선 탐색  (0) 2020.12.18
그래프의 개념과 구현  (0) 2020.12.17
순차 탐색과 이진 탐색  (0) 2020.12.17
우선순위 큐  (0) 2020.12.17
Posted by khon98
,

그래프(Graph)

- 사물을 정점과 간선으로 나타내기 위한 도구

- 그래프는 두 가지 방식으로 구현할 수 있음

 

1. 인접 행렬(Adjacency Matrix) : 2차원 배열을 사용하는 방식

 - 인접 행렬에서는 그래프를 2차원 배열로 표현

- 모든 정점들의 연결 여부를 저장하여 O(V2)의 공간을 요구하므로 공간 효율성이 떨어지지만 두 정점이 서로 연결되어 있는지 확인하기 위해 O(1)의 시간을 요구

 

2. 인접 리스트(Adjacency List) : 리스트를 사용하는 방식

- 연결된 간선의 정보만을 저장하여 O(E)의 공간을 요구하므로 공간 효율성이 우수하지만 두 정점이 서로 연결되어 있는지 확인하기 위해 O(V)의 시간을 요구

 

무방향 비가중치 그래프와 인접 행렬

- 모든 간선이 방향성을 가지지 않는 그래프를 무방향 그래프라고 함

- 모든 간선에 가중치가 없는 그래프를 비가중치 그래프라고 함

- 무방향 비가중치 그래프가 주어졌을 때 연결되어 있는 상황을 인접 행렬로 출력할 수 있음

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

int a[1001][1001];
int n, m;

int main(void) {
    scanf("%d %d", &n, &m);
    for (int i = 0; i < m; i++) {
        int x, y;
        scanf("%d %d", &x, &y);
        a[x][y] = 1;
        a[y][x] = 1;
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            printf("%d ", a[i][j]);
        }
        printf("\n");
    }
    system("pause");
}

 

방향 가중치 그래프와 인접 리스트

- 모든 간선이 방향을 가지는 그래프를 방향 그래프라고 함

- 모든 간선에 가중치가 있는 그래프를 가중치 그래프라고 함

- 방향 가중치 그래프가 주어졌을 때 연결되어 있는 상황을 인접 리스트로 출력할 수 있음

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int index;
    int distance;
    struct Node *next;
} Node;

void addFront(Node *root, int index, int distance) {
    Node *node = (Node*) malloc(sizeof(Node));
    node->index = index;
    node->distance = distance;
    node->next = root->next;
    root->next = node;
}

void showAll(Node *root) {
    Node *cur = root->next;
    while (cur != NULL) {
        printf("%d(거리: %d) ", cur->index, cur->distance);
        cur = cur->next;
    }
}

int main(void) {
    int n, m;
    scanf("%d %d", &n, &m);
    Node** a = (Node**)malloc(sizeof(Node*) * (n + 1));
    for (int i = 1; i <= n; i++) {
        a[i] = (Node*)malloc(sizeof(Node));
        a[i]->next = NULL;
    }
    for (int i = 0; i < m; i++) {
        int x, y, distance = 0;
        scanf("%d %d %d", &x, &y, &distance);
        addFront(a[x], y, distance);
    }
    for (int i = 1; i <= n; i++) {
        printf("원소 [%d]: ", i);
        showAll(a[i]);
        printf("\n");
    }
    system("pause");
    return 0;
}

'자료구조 알고리즘' 카테고리의 다른 글

너비 우선 탐색  (0) 2020.12.18
깊이 우선 탐색  (0) 2020.12.17
순차 탐색과 이진 탐색  (0) 2020.12.17
우선순위 큐  (0) 2020.12.17
이진 트리의 구현 및 순회  (0) 2020.12.17
Posted by khon98
,

순차 탐색(Sequential Search)

- 특정한 원소를 찾기 위해 원소를 순차적으로 하나씩 탐색하는 방법

- O(N)의 시간 복잡도를 가짐

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LENGTH 1000

char **array;
int founded;

int main(void) {
    int n;
    char *word;
    word = malloc(sizeof(char) * LENGTH);
    scanf("%d %s", &n, word);
    array = (char**) malloc(sizeof(char*) * n);
    for (int i = 0; i < n; i++) {
        array[i] = malloc(sizeof(char) * LENGTH);
        scanf("%s", array[i]);
    }
    for (int i = 0; i < n; i++) {
        if (! strcmp(array[i], word)) {
            printf("%d번째 원소.\n", i + 1);
            founded = 1;
        }
    }
    if (!founded)printf("원소를 찾을 수 없음.\n");
    system("pause");
    return 0;
}

 

이진 탐색(Binary Search)

- 배열 내부 데이터가 이미 정렬되어 있는 상황에서 사용 가능한 알고리즘

- 탐색 범위를 절반씩 좁혀가며 데이터를 탐색하는 특징이 있음

- 한 번 확인할 때마다 보아야 하는 원소의 개수가 절반씩 줄어든다는 점에서 탐색 시간이 O(logN)의 시간 복잡도를 가짐

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100000

int a[MAX_SIZE];
int founded = 0;

int search(int start, int end, int target) {
    if (start > end) return -9999;
    int mid = (start + end) / 2;
    if (a[mid] == target) return mid;
    else if (a[mid] > target) return search(start, mid - 1, target);
    else return search(mid + 1, end, target);
}

int main(void) {
    int n, target;
    scanf("%d %d", &n, &target);
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);
    int result = search(0, n - 1, target);
    if (result != -9999) printf("%d번째 원소.\n", result + 1);
    else printf("원소를 찾을 수 없음.\n");
    system("pause");
    return 0;
}

'자료구조 알고리즘' 카테고리의 다른 글

깊이 우선 탐색  (0) 2020.12.17
그래프의 개념과 구현  (0) 2020.12.17
우선순위 큐  (0) 2020.12.17
이진 트리의 구현 및 순회  (0) 2020.12.17
이진 트리의 개념  (0) 2020.12.17
Posted by khon98
,

우선순위 큐의 필요성

- 우선순위 큐는 우선순위를 가진 데이터들을 저장하는 큐를 의미

- 데이터를 꺼낼 때 우선순위가 높은 데이터가 가장 먼저 나온다는 특징이 있어 많이 활용되고 있음

- 우선순위 큐는 운영체제의 작업 스케줄링, 정렬, 네트워크 관리 등의 다양한 기술에 적용되고 있음

 

우선순위 큐의 차이점

- 일반적인 형태의 큐는 선형적인 형태를 가지고 있지만 우선순위 큐는 트리 구조로 보는 것이 합리적

- 일반적으로 우선순위 큐는 최대 힙을 이용해 구현함

 

최대 힙

- 최대 힙은 부모 노드가 자식 노드보다 값이 큰 완전 이진트리를 의미

- 최대 힙의 루트 노드는 전체 트리에서 가장 큰 값을 가진다는 특징이 있음

- 항상 전체 트리가 최대 힙 구조를 유지하도록 자료구조를 만들 수 있음

- PUSH : 우선순위 큐에 데이터를 삽입

- POP : 우선순위 큐에서 데이터를 추출

 

우선순위 큐의 삽입

- 삽입할 원소는 완전 이진트리를 유지하는 형태로 순차적으로 삽입됨

- 삽입 이후에는 루트 노드까지 거슬러 올라가면서 최대 힙을 구성함 [상향식]

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 10000

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

typedef struct {
    int heap[MAX_SIZE];
    int count;
} priorityQueue;

void push(priorityQueue *pq, int data) {
    if (pq->count >= MAX_SIZE) return;
    pq->heap[pq->count] = data;
    int now = pq->count;
    int parent = (pq->count - 1) / 2;
    // 새 원소를 삽입한 이후에 상향식으로 힙을 구성
    while (now > 0 && pq->heap[now] > pq->heap[parent]) {
        swap(&pq->heap[now], &pq->heap[parent]);
        now = parent;
        parent = (parent - 1) / 2;
    }
    pq->count++;
}

 

우선순위 큐의 삭제

- 삭제할 때는 루트 노드를 삭제하고 가장 마지막 원소를 루트 노드의 위치로 옮겨줌

- 삭제 이후에는 리프 노드까지 내려가면서 최대 힙을 구성 [하향식]

 

* 우선순위 큐의 추출

int pop(priorityQueue *pq) {
    if (pq->count <= 0) return -9999;
    int res = pq->heap[0];
    pq->count--;
    pq->heap[0] = pq->heap[pq->count];
    int now = 0, leftChild = 1, rightChild = 2;
    int target = now;
    // 새 원소를 추출한 이후에 하향식으로 힙을 구성
    while (leftChild < pq->count) {
        if (pq->heap[target] < pq->heap[leftChild]) target = leftChild;
        if (pq->heap[target] < pq->heap[rightChild] && rightChild < pq->count) target = rightChild;
        if (target == now) break; // 더 이상 내려가지 않아도 될 때 종료
        else {
            swap(&pq->heap[now], &pq->heap[target]);
            now = target;
            leftChild = now * 2 + 1;
            rightChild = now * 2 + 1;
        }
    }
    return res;
}

 

* 우선순위 큐의 사용

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 10000

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

typedef struct {
    int heap[MAX_SIZE];
    int count;
} priorityQueue;

void push(priorityQueue *pq, int data) {
    if (pq->count >= MAX_SIZE) return;
    pq->heap[pq->count] = data;
    int now = pq->count;
    int parent = (pq->count - 1) / 2;
    // 새 원소를 삽입한 이후에 상향식으로 힙을 구성
    while (now > 0 && pq->heap[now] > pq->heap[parent]) {
        swap(&pq->heap[now], &pq->heap[parent]);
        now = parent;
        parent = (parent - 1) / 2;
    }
    pq->count++;
}

int pop(priorityQueue *pq) {
    if (pq->count <= 0) return -9999;
    int res = pq->heap[0];
    pq->count--;
    pq->heap[0] = pq->heap[pq->count];
    int now = 0, leftChild = 1, rightChild = 2;
    int target = now;
    // 새 원소를 추출한 이후에 하향식으로 힙을 구성
    while (leftChild < pq->count) {
        if (pq->heap[target] < pq->heap[leftChild]) target = leftChild;
        if (pq->heap[target] < pq->heap[rightChild] && rightChild < pq->count) target = rightChild;
        if (target == now) break; // 더 이상 내려가지 않아도 될 때 종료
        else {
            swap(&pq->heap[now], &pq->heap[target]);
            now = target;
            leftChild = now * 2 + 1;
            rightChild = now * 2 + 1;
        }
    }
    return res;
}

int main(void) {
    int n, data;
    scanf("%d", &n);
    priorityQueue pq;
    pq.count = 0;
    for (int i = 0; i < n; i++) {
        scanf("%d", &data);
        push(&pq, data);
    }
    for (int i = 0; i < n; i++) {
        int data = pop(&pq);
        printf("%d ", data);
    }
    system("pause");
    return 0;
}

'자료구조 알고리즘' 카테고리의 다른 글

그래프의 개념과 구현  (0) 2020.12.17
순차 탐색과 이진 탐색  (0) 2020.12.17
이진 트리의 구현 및 순회  (0) 2020.12.17
이진 트리의 개념  (0) 2020.12.17
기수 정렬  (0) 2020.12.17
Posted by khon98
,

이진 트리

- 이진 트리는 포인터를 이용하여 구현하면 효과적인 데이터 관리가 가능

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int data;
    struct Node *leftChild;
    struct Node *rightChild;
} Node;

Node* initNode(int data, Node* leftChid, Node* rightChild) {
    Node* node = (Node*)malloc(sizeof(Node));
    node->data = data;
    node->leftChild = leftChid;
    node->rightChild = rightChild;
    return node;
}

 

이진 트리 순회

- 이진 트리에 담긴 데이터를 하나씩 방문하는 방법으로는 대표적으로 세 가지가 있음

 

1. 전위 순회

- 자기 자신을 출력

- 왼쪽 자식을 방문

- 오른쪽 자식을 방문

void preorder(Node* root) {
    if (root) {
        printf("%d ", root->data);
        preorder(root->leftChild);
        preorder(root->rightChild);
    }
}

 

2. 중위 순회

- 왼쪽 자식을 방문

- 자기 자신을 출력

- 오른쪽 자식을 방문

void inorder(Node* root) {
    if (root) {
        preorder(root->leftChild);
        printf("%d ", root->data);
        preorder(root->rightChild);
    }
}

 

3. 후위 순회

- 왼쪽 자식을 방문

- 오른쪽 자식을 방문

- 자기 자신을 출력

void postorder(Node* root) {
    if (root) {
        preorder(root->leftChild);
        preorder(root->rightChild);
        printf("%d ", root->data);
    }
}

 

* 이진 트리 사용해보기

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int data;
    struct Node *leftChild;
    struct Node *rightChild;
} Node;

Node* initNode(int data, Node* leftChid, Node* rightChild) {
    Node* node = (Node*)malloc(sizeof(Node));
    node->data = data;
    node->leftChild = leftChid;
    node->rightChild = rightChild;
    return node;
}

void preorder(Node* root) {
    if (root) {
        printf("%d ", root->data);
        preorder(root->leftChild);
        preorder(root->rightChild);
    }
}

void inorder(Node* root) {
    if (root) {
        preorder(root->leftChild);
        printf("%d ", root->data);
        preorder(root->rightChild);
    }
}

void postorder(Node* root) {
    if (root) {
        preorder(root->leftChild);
        preorder(root->rightChild);
        printf("%d ", root->data);
    }
}

int main(void) {
    Node* n7 = initNode(50, NULL, NULL);
    Node* n6 = initNode(37, NULL, NULL);
    Node* n5 = initNode(23, NULL, NULL);
    Node* n4 = initNode(5, NULL, NULL);
    Node* n3 = initNode(48, n6, n7);
    Node* n2 = initNode(17, n4, n5);
    Node* n1 = initNode(30, n2, n3);
    preorder(n1);
    printf("\n");
    inorder(n1);
    printf("\n");
    postorder(n1);
    system("pause");
    return 0;
}

'자료구조 알고리즘' 카테고리의 다른 글

순차 탐색과 이진 탐색  (0) 2020.12.17
우선순위 큐  (0) 2020.12.17
이진 트리의 개념  (0) 2020.12.17
기수 정렬  (0) 2020.12.17
계수 정렬  (0) 2020.12.17
Posted by khon98
,

트리

- 나무의 형태를 뒤집은 것과 같은 형태의 자료 구조

- 길이란 출발 노드에서 목적지 노드까지 거쳐야 하는 가짓수를 의미

- 깊이란 루트 노드에서 특정 노드까지의 길이를 의미

- 트리의 높이란 루트 노드에서 가장 깊은 노드까지의 길이

 

이진 트리

- 최대 2개의 자식을 가질 수 있는 트리

 

포화 이진 트리

- 리프 노드를 제외한 모든 노드가 두 자식을 가지고 있는 트리

 

완전 이진 트리

- 모든 노드들이 왼쪽 자식부터 차근차근 채워진 노드

 

높이 균형 트리

- 왼쪽 자식 트리와 오른쪽 자식 트리의 높이가 1 이상 차이 나지 않는 트리

 

 

- 이진 트리는 많은 양의 노드를 낮은 높이에서 관리할 수 있다는 점에서 데이터 활용의 효율성이 높아짐

- 데이터 저장, 탐색 등의 다양한 목적에서 사용할 수 있음

'자료구조 알고리즘' 카테고리의 다른 글

우선순위 큐  (0) 2020.12.17
이진 트리의 구현 및 순회  (0) 2020.12.17
기수 정렬  (0) 2020.12.17
계수 정렬  (0) 2020.12.17
퀵 정렬  (0) 2020.12.11
Posted by khon98
,