해시(Hash)

  해시는 데이터를 최대한 빠른 속도로 관리할 수 있도록 도와주는 자료구조이다.

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

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

 

동작과정

  입력데이터를

  78/AA

  12/BB

  55/CC

  64/DD

  라고 입력하고 해시함수를 (n mod 10)으로 입력값 n을 10으로 나눈 나머지값을 키(key)로 갖게 한다면

  인덱스 8의 값으로 AA가 들어가고 2에는 BB, 5에는 CC, 4에는 DD가 값(value)로 들어가게 된다.

 

  이런식으로 해시함수를 이용하게 되면 기존의 값들이 특정한 값으로 매칭이 되어서 차곡차곡 쌓이게 되는

  형태다.

  특정한 값(value)을 찾을때는 키(key)로 접근할 수 있는데 배열에서의 index를 의미하는것과 같다.

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

  값에 접근할 수 있다.

 

해싱(Hashing)

  해싱 알고리즘은 나눗셈법(Division Method)이 가장 많이 활용된다. 입력값을 테이블의 크기로 나눈 나머지를

  키로 이용하는 방식이다. 테이블의 크기는 소수(Prime Number)로 설정하는 것이 효율성이 높다.

 

해시의 충돌(Collision)

  해시함수의 입력값으로는 어떠한 값이나 모두 들어갈 수 있지만, 해시함수를 거쳐 생성되는 키의 경우의수는

  한정적이기 때문에 키 중복이 발생할 수 있다.

  해시테이블에서 키가 중복되는 경우를 충돌(Collision)이라고 표현한다.

 

  입력데이터가 77/AA, 17/BB라고 하고 해시함수를 (n mod 10)이라고 했을 때 두 데이터 모두 7이라는

  키값을 갖게 된다. 이러한 문제가 발생하는 것이 충돌이다.

 

  아무리 잘 만들어진 해시함수라도 충돌이 발생할 수 밖에 없다. 충돌을 처리하는 방법은 다음과 같이

  두가지유형으로 분류할 수 있다.

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

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

 

선형조사법

  입력데이터가 1/AA, 5/BB, 10012/CC, 20019/DD일 때 해시함수로 (n mod 10007)처리한다고 하면

  1과 5는 일단 값이 들어간다. 10012를 10007로 나눈 나머지값은 5가 되는데 이미 5라는 키값은 존재한다.

  그럼 중복이 되기때문에 다음 인덱스로 들어간다. 그럼 1에는 AA, 5에는 BB, 6에는 CC가 들어가게 된다.

  마지막 데이터인 20019역시 나머지값이 5이므로 CC가 들어갈때와 동일하게 중복이 된다.

  그럼 5에서 중복이기때문에 6으로 가지만 6역시 키값이 존재하므로 7에 값이 들어간다.

  이렇게 차례로 하나씩 탐색하는것 즉, 선형적으로 탐색한다고 해서 선형조사법이라고 한다.

 

선형조사법 예제코드

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h> //time을 사용하는 이유는 무작위 데이터를 5000개 삽입할 것이기 때문
#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;
    //각 테이블의 포인터값을 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 search(Student** hashTable, int id){
  int i = id % TABLE_SIZE;
  
  while(1){
    if(hashTable[i % TABLE_SIZE] == NULL) return -1;
    //해당 인덱스가 비어있다면 -1을 리턴
    else if(hashTable[i % TABLE_SIZE]->id == id) return i;
    //해당 인덱스의 id가 id와 같다면 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;
    //랜덤으로 만들어진 숫자를 1000000으로 나눈 나머지값
    //10007을 넘기지 않기 위함이다.
    sprintf(student->name, "사람%d", student->id);
    //학생의 이름은 사람+학생번호로 만들어준다.
    
    if(search(hashTable, student->id) == -1){
      //중복되는 ID는 존재하지 않도록
      //해당 학생 번호에 해당하는 key값을 테이블에서 매칭하도록 찾아야 한다.
      int index = findEmpty(hashTable, student->id);
      add(hashTable, student, index);
    }
  }
  
  show(hashTable);
  destructor(hashTable);
  system("pause");
  return 0;
}

  실행해보면 많은 데이터가 삽입되었지만 중복된 키값은 없다는 것을 확인할 수 있다.

  그리고 설정한것처럼 10007이 넘는 키값은 존재하지 않는다.

 

선형조사법의 단점

  선형조사법은 충돌이 발생하기 시작하면 유사한 값을 갖는 데이터끼리 서로 밀집되는 집중결합문제가 존재한다.

  예를들어 데이터 5라는 키에 들어가야되는 값이 3개 존재했다면 5, 6, 7에 순차적으로 들어가 밀집되는 문제다.

  선형조사법이라고 해도 해시테이블의 크기가 비약적으로 크다면, 다시말해 메모리를 많이 소모한다면 충돌이 적게

  발생하므로 매우 빠른 데이터 접근속도를 가질 수 있다.

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

 

 

이차조사법

  선형조사법의 단점을 보완하기 위한 조사법이다.

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

  아까와 같은 1, 5, 10012, 20019라는 데이터와 (n mod 10007)이라는 해시 함수를 사용한다면

  10012부터 충돌이 되는데 처음 중복시에는 다음 인덱스만 체크해서 넣어준다.

  그 다음 20019에서 또 충돌이 발생했을 때는 2의 제곱인 4만큼의 공간을 넘어가서 입력하는 방식이다.

 

조사법 테이블 크기 재설정

  일반적으로 선형조사법, 이차조사법 등의 조사법에서 테이블이 가득차게 되면 테이블의 크기를 확장하여 계속해서

  해시테이블을 유지할 수 있도록 한다.

 

 

체이닝(Chaning)

  체이닝 기법은 연결리스트를 활용해 특정한 키를 갖는 항목들을 연결하여 젖앟나다.

  연결리스트를 사용한다는 점에서 삽입과 삭제가 용이하다. 또한 테이블 크기 재설정 문제는 '동적할당'을 통해서

  손쉽게 해결할 수 잇다. 다만 동적할당을 위한 추가적인 메모리공간이 소모된다는 단점이 있다.

 

  연결리스트를 사용한다는 것은 5/BB, 10012/CC, 20019/DD의 데이터가 (n mod 10007)의 해시함수를 통했을 때

  키값이 5로 중복되는데 이것을 5라는 하나의 키에 BB, CC, DD 형태의 연결리스트로 만들어 해결하는 것이다.

 

체이닝 예제코드

 

#define _CRT_SECURE_NO_WARINGS
#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;

//학생구조체가 연결리스트 형태로 들어갈 수 있도록 각각의 bucket을 설정
//즉, 하나의 키에 해당하는 데이터가 여러개 들어갈 수 있다는 것
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; //해당 테이블이 NULL이라면
  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)) {//중복체크
      add(hashTable, student);
      //중복이 되지 않는 경우만 삽입
    }
  }
  
  show(hashTable);
  destructor(hashTable);
  system("pause");
  return 0;
}

  선형기법과 다르게 동일한 키값에 다른 데이터가 들어가있는것을 확인할 수 있다.

  체이닝 기법을 활용해 동일한 키값에 연결리스트로 들어갔다는 것을 확인할 수 있는 것이다.

 

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

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

프림(Prim) 알고리즘  (0) 2021.01.19
최소신장트리(Minimum Spanning Tree, MST)  (0) 2021.01.19
AVL트리  (0) 2021.01.17
이진탐색트리(Binary Search Tree)  (0) 2021.01.16
너비우선탐색(BFS)  (0) 2021.01.15

AVL트리

  AVL트리는 균형이 갖춰진 이진트리를 의미한다.

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

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

 

  AVL트리는 균형인수(Balance Facter)라는 개념을 이용한다.

  균형인수는 왼쪽자식높이에서 오른쪽자식높이를 뺀 절대값을 의미한다.

  이 균형인수가 ±1 이하인 경우가 AVL트리이다.

  균형인수가 범위를 벗어난 경우에는 회전(rotation)을 통해 재구성해야 한다.

  AVL트리의 각 노드는 균형인수를 계산하기 위한 목적으로 자신의 높이값을 갖는다.

 

  AVL트리는 4가지 형식에 의해 균형이 깨질 수 있다. 균형인수가 깨지는 노드를 X라고 했을 때

  4가지 형식은 다음과 같다.

 

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

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

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

  RL형식 : X의 오른쪽자식노드의 왼쪽에 삽입하는 경우

 

AVL트리의 균형잡기

  AVL트리의 균형잡기는 각 노드가 삽입될 때마다 수행되어야 한다. 삽입과정에 소요되는 시간 복잡도는 O(logN)이다.

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

  만족해야 한다.

 

AVL트리의 삽입

  일반적인 이진탐색트리와 과정이 흡사하다. 다만 삽입시에 거치는 모든 노드에 대하여 균형잡기를 수행해줘야

  한다는 특징이 있다.

 

AVL트리의 출력

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

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

  

예제코드

 

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

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;

int getHeight(Node* node){ //현재 특정한 루트노드에서의 높이값을 반환
  if(node == NULL) return 0; //노드가 없으니까 0을 반환
  return node->height;
}

void setHeight(Node* node){
  node->height = getMax(getHeight(node->leftChild), getHeight(node->rightChild)) + 1;
  //왼쪽, 오른쪽 중 더 큰 값에 + 1을 해준다.
}

int getDifference(Node* node){ //균형인수를 구하는 함수
  if(node == NULL) return 0;
  Node* leftChild = node->leftChild;
  Node* rightChild = node->rightChild;
  return getHeight(leftChild) - getHeight(rightChild);
}

Node* rotateLL(Node* node){
  Node* leftChild = node->leftChild; 
  //node의 왼쪽 자식노드를 leftChild로 정의
  
  node->leftChild = leftChild->rightChild; 
  //node의 왼쪽 자식노드를 leftChild로 정의한 노드의 오른쪽 자식노드로 변경
  
  leftChild->rightChild = node;
  //leftChild로 정의한 노드의 오른쪽 자식노드를 node로 변경.
  
  setHeight(node); //회전이후에 높이를 다시 계산
  return leftChild;
}

Node* rotateRR(Node* node){
  Node* rightChild = node->rightChild;
  node->rightChild = rightChild->leftChild;
  rightChild->leftChild = node;
  setHeight(node);
  return rightChild;
}

Node* rotateLR(Node* node){
  Node* leftChild = node->leftChild;
  node->leftChild = rotateRR(leftChild);
  setHeight(node->leftChild);
  return rotateLL(node);
}

Node* rotateRL(Node* node){
  Node* rightChild = node->rightChild;
  node->rightChild = rotateLL(rightChild);
  setHeight(node->rightChild);
  return rotateRR(node);
}

Node* balance(Node* node){ //균형잡는 함수
  int difference = getdifference(node); //균형인수
  if(difference >= 2){ //균형인수가 2이상인 경우에는 LL인지 LR인지 확인 후 처리 
    if(getDifference(node->leftChild) >= 1){
      node = rotateLL(node);
    }
    else {
      node = rotateLR(node);
    }
  }
  else if(difference <= -2){ //-2이하인 경우 RR인지 RL인지 확인 후 처리
    if(getDifference(node->rightChild) <= -1){
      node = rotateRR(node);
    }
    else{
      node = rotateRL(node);
    }
  }
  setHeight(node); //회전 이후에 높이를 다시 계산
  return node;
}

Node* insertNode(Node* node, int data){ //삽입함수
  if(!node){
    node = (Node*)malloc(sizeof(Node));
    node->data = data;
    node->height = 1; //초기 높이값을 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;
}

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("        ");
      //가장 오른쪽으로 들어간 level값, 해당 노드의 레벨만큼 띄어쓰기를 해줌으로써
      //가장 깊은 노드가 가장 오른쪽에 출력되도록 한다.
    }
    
    printf("%d(%d)", node->data, getHeight(node));//해당값을 출력하고
    display(node->leftChild, level + 1);
    //왼쪽으로 재귀적으로 호출해서 기존 트리구조의 오른쪽 자식노드부터 출력하고
    //나중에 왼쪽자식노드가 출력될 수 있도록 한다.
  }
}

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);
  
  display(root, 1);
  printf("\n");
  system("pause);
}


                                23(1)
                        21(2)
                                19(1)
                18(3)
                                16(1)
                        15(2)
                                14(1)
Root : 12(4)
                                9(1)
                        8(2)
                                7(1)
                6(3)
                                5(1)
                        3(2)
                                1(1)

  실행하면 이렇게 출력된다.

  

  처음 강의 들으면서 LL, RR, LR, RL 회전 그림을 보면서도 이해가 하나도 안되서 여기저기 찾아보고 코드 뜯어보면서

  이해했다.

 

 

  LL회전

    예제코드의 rotateLL에서 node가 A라고 할 때

    Node leftChild = node(A)->leftChild = B

    node(A)->leftChild = leftChild(B)->rightChild = E

 

    leftChild(B)->rightChild = node(A);

    이렇게 처리되는것이다.

 

 

  RR회전

    RR회전인 rotateRR은 LL의 반대이다.

    Node rightChild = node(B)->rightChild = A

    node(B)->rightChild = rightChild(A)->leftChild = E

    rightChild(A)->leftChild = node(B)

    LL의 반대이므로 LL 처리가 된 상태에서 RR을 처리한다면 다시 원래대로 돌아오는게 맞다.

 

  LR회전

   

    Node leftChild = node(A)->leftChild = B

    node(A)->leftChild = rotateRR(leftChild(B)) {

                                  Node rightChild = node(B)->rightChild = E

                                  node(B)->rightChild = rightChild(E)->leftChild(F)

                                  rightChild(E)->leftChild = node(B)

                                  return rightChild(E)

                                }

    return rotateLL(node(A)) {

               Node leftChild = node(A)->leftChild = E

               node(A)->leftChild = leftChild(E)->rightChild = G

               leftChild(E)->rightChild = node(A)

 

  RL회전

    Node rightChild = node(A)->rightChild = C

    node->rightChild = rotateLL(rightChild(C)) {

                                Node leftChild = node(C)->leftChild = D

                                node(C)->leftChild = leftChild(D)->rightChild = G

                                leftChild(D)->rightChild = node(C)

                                return leftChild(D)

                              }

    return rotateRR(node(A)) {

               Node rightChild = node(A)->rightChild = D

               node(A)->rightChild = rightChild(D)->leftChild = F

               rightChild(D)->leftChild = node(A);

             }

 

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

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

최소신장트리(Minimum Spanning Tree, MST)  (0) 2021.01.19
해시(Hash)  (0) 2021.01.18
이진탐색트리(Binary Search Tree)  (0) 2021.01.16
너비우선탐색(BFS)  (0) 2021.01.15
깊이우선탐색(DFS)  (0) 2021.01.14

이진탐색트리(Binary Search Tree)

  이진탐색(Binary Search)이 항상 동작하도록 구현하여 탐색속도를 극대화시킨 자료구조를 이진탐색트리라고 한다.

 

  이진탐색트리에서는 항상 부모노드가 왼쪽자식노드보다는 크고 오른쪽자식노드보다는 작다.

 

탐색방법

 

  여기서 37을 찾는다고 했을 때 루트노드인 30과 비교한다.

  그럼 찾는값보다 루트노드가 더 작기 때문에 더 큰값들이 있는 오른쪽 자식노드로 이동한다.

  오른쪽 자식노드인 48은 찾는값인 37보다 크기때문에 더 작은값인 왼쪽 자식노드로 이동한다.

  

  이와같이 한 노드의 값을 찾을 값과 비교하면 한쪽을 제외하고 검색할 수 있다.

  한번 확인할때마다 확인해야 하는 원소의 개수가 절반씩 줄어든다는 점에서 완전이진트리인 경우

  탐색시간에 O(logN)의 시간 복잡도를 갖는다.

 

이진탐색트리의 삭제

  1. 자식노드가 없는 경우

    삭제할 노드의 자식노드가 없는 경우 단순히 제거만 하면 된다.

 

  2. 자식노드가 하나만 존재하는 경우

     

    48처럼 하나의 자식노드를 갖고 있다면 삭제할 노드의 자리에 자식노드를 넣어준다.

    48을 삭제한다고 하면 37을 48의 위치로 바꿔주면 된다.

 

  3. 자식노드가 둘다 존재하는 경우

    위 그림에서 30처럼 자식노드가 둘다 존재한다면 삭제할 노드의 자리에 그 보다 큰 값들 중

    바로 다음으로 큰값을 넣어준다.

    30을 삭제한다면 다음으로 큰 값인 37을 30위치에 넣어준다.

 

예제코드

 

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

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

Node* insertNode(Node* root, int data){ //특정 root에서 어떠한 노드를 삽입하고자 한다면
  if(root == NULL){ //그 루트가 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 preorder(Node* root){ //순회
  if(root == NULL) return;
  printf("%d ", root->data);
  preorder(root->leftChild);
  preorder(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;
      //왼쪽 자식노드가 NULL이 아니라면 노드에 왼쪽 자식노드를 담고
      //NULL이라면 오른쪽 자식노드를 담는다.
      free(node);
      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 = deleteNode(root, 30);
  preorder(root);
  system("pause");
}

  삽입처리된 트리의 결과를 확인할 수 있다.

  원하는 값을 찾아 출력하는 예제코드는 아니다.

  root = deleteNode(root, 30);을 주석처리하고 실행하면

  출력값은 30 17 5 23 48 37 50 으로 출력되는데 위에 탐색방법에 있는 그림과 같은 형태다.

  preorder에서 처음에 루트값인 30을 먼저 출력하고 leftChild를 먼저 탐색하기 때문에 17 5 23이 출력되고

  그 다음 rightChild에서 48 37 50 이 출력된다.

 

  그리고 삭제함수를 포함해서 실행한다면 37 17 5 23 48 50이 출력된다.

  30을 삭제하고 그 바로 다음으로 큰 값인 37이 root로 올라오게 되는것을 확인할 수 있다.

 

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

  해야 한다.

  

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

  트리에서 데이터의 개수가 N개일 때 배열과 마찬가지로 O(N)의 공간만이 소요되며 삽입 및 삭제에 있어서

  일반적인 경우 기존의 배열을 이용하는 방식보다 효율적이다. 그래서 데이터베이스 등 대용량 저장 및 검색

  자료구조로 많이 사용된다.

  

  한쪽으로 치우친 편향 이진트리(Skwed Binary Tree)의 경우 탐색에 있어 O(N)의 시간 복잡도가 형성되므로

  기존의 배열을 사용하는 것 보다 오히려 많은 공간과 시간이 낭비된다.

 

  따라서 이진트리를 만들때는 트리의 균형이 맞도록 설정하는 것이 중요하다.

  

 

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

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

해시(Hash)  (0) 2021.01.18
AVL트리  (0) 2021.01.17
너비우선탐색(BFS)  (0) 2021.01.15
깊이우선탐색(DFS)  (0) 2021.01.14
그래프(Graph)  (0) 2021.01.13

너비우선탐색(Breadth First Search)

  너비우선탐색은 너비를 우선으로 하여 탐색을 수행하는 알고리즘이다.

  깊이우선탐색(DFS)와 마찬가지로 맹목적으로 전체노드를 탐색하고자 할 때 자주 사용되며

  큐(Queue)자료구조에 기초한다.

 

  너비우선 탐색은 고급 그래프 탐색 알고리즘에서 자주 활용된다.

  큐 자료구조에 기초한다는 점에서 구현이 간단하다.

  구현함에 있으서 큐STL을 사용하면 좋으며 탐색을 수행함에 있어서 O(N)의 시간이 소요된다.

  일반적인 경우 실제 수행시간은 DFS보다 좋은 편이다.

 

탐색과정

  탐색시작 노드를 큐에 삽입하고 방문처리한다.

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

  위 과정을 더이상 수행할 수 없을 때 까지 반복한다.

 

  이와같은 연결리스트가 있고 1부터 시작한다고 했을 때, 큐에 1이 먼저 들어가게 된다.

1        

  그리고 1과 인접한 모든 노드를 큐에 넣어주면서 1은 빼준다.

2 3 8    

  그 다음 2와 인접한 노드를 넣어주면서 2를 뺀다.

3 8 7    

  3을 빼면서 3과 인접한 4,5를 큐에 넣는다

8 7 4 5  

  8은 인접한 노드가 없으므로 큐에서 빼주기만한다.

7 4 5    

  7을 빼면서 인접노드 6을 큐에 넣는다.

4 5 6    

  4, 5, 6은 방문하지 않은 인접노드가 없으므로 큐에서 빼준다.

 

  그래서 방문순서 즉, 출력순서는

  1-2-3-8-7-4-5-6이 된다.

 

예제코드

 

#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 enQueue(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 deQueue(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;//큐 초기화
  enQueue(&q, start);
  c[start] = 1;
  //방문처리
  
  while(q.count != 0){//큐에 아무것도 없을때까지 반복
    int x = deQueue(&q);
    printf("%d ", x); //추출한 원소 출력
    Node *cur = a[x]->next; //인접한 노드
    
    while(cur != NULL){
      int next = cur->index;
      if(!c[next]){ //방문하지 않은 상태라면
        enQueue(&q, next); //큐에 삽입
        c[next] = 1; //방문처리
      }
      cur = cur->next; //반복을 위해 next
    }
  }
}

int main(void){
  scanf("%d %d", &n, &m);
  a = (Node**)malloc(sizeof(Node*) * (MAX_SIZE));
  
  for(int i = 1; i <= n; i++){ //1부터 n까지의 정점에 대해 연결리스트 초기화
    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); //노드 1을 기준으로 너비우선탐색 실행
  system("pause");
  return 0;
}

  5 4

  1 2

  1 3

  1 5

  2 5

  이렇게 입력하면

  1 5 3 2가 출력된다.

 

  5개의 정점과 4개의 간선이 있도록 하고

  각 노드를 만들며 next를 NULL로 초기화한다.

 

  연결리스트에 추가하는 부분을 보면 a[x], y와 a[y], x가 있는데 a[1], 2, a[2], 1 이 형태로 들어간다고 가정하고

  처리를 본다면

  node->index = 2

  node->next = root(a[1])->next(NULL) = NULL

  root(a[1])->next = node(index2)

 

  node->index = 1

  node->next = root(a[2])->next(NULL) = NULL

  root(a[2])->next = node(index1)

 

  이런 식으로 2와 1을 서로 인접한 노드로 만들어주는 것이다.

  그림으로 보면 다음과 같다.

  

  1부터 시작한다고 했으니 큐에 처음 1을 넣어준다음 방문처리를 한다.

  그리고 반복문으로 큐에 아무것도 남지 않을때까지 반복하도록 하고 큐에 담겨있는 원소를 추출한다.

  그럼 제일 먼저 시작 노드인 1이 빠져나오면서 출력되고 1의 인접노드를 cur에 담아 내부 반복문을

  수행한다. 내부반복문에서는 해당 노드의 모든 인접 노드를 방문하며 큐에 삽입해주고 방문처리를 한다.

  그럼 5, 3, 2순서로 큐에 들어가게 되고 다시 deQueue로 하나 추출한 다음 그 노드의 인접노드를 체크한다.

  여기서는 이미 1을 제외한 모든 노드인 5, 3, 2를 다 방문해서 큐에 넣어뒀기 때문에 체크만 하고

  하나씩 출력하게 된다.

  

  노드 1의 연결 순서가 5, 3, 2가 되는것은 연결리스트로 처음 들어간 2가 다음에 들어온 3의 next가 되고

  3은 그 뒤에 들어온 5의 next가 되기 때문이다.

 

  

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

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

AVL트리  (0) 2021.01.17
이진탐색트리(Binary Search Tree)  (0) 2021.01.16
깊이우선탐색(DFS)  (0) 2021.01.14
그래프(Graph)  (0) 2021.01.13
순차탐색, 이진탐색  (0) 2021.01.12

깊이우선탐색(Depth First Search)

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

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

  스택(Stack) 자료구조에 기초한다.

 

  빠르게 모든 경우의 수를 탐색하고자 할 때 쉽게 사용할 수 있으며

  다음과 같은 알고리즘에 따라 구현할 수 있다.

 

  탐색 시작 노드를 스택에 삽입하고 방문처리를 한다.

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

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

  위 과정을 더이상 수행할 수 없을 때 까지 반복한다.

 

깊이우선탐색 수행과정

 

  이러한 형태의 그래프를 탐색하며 1부터 출발한다고 가정한다.

  1부터 출발하면 1에서 연결된 노드 중 방문하지 않은 2를 방문하고 2에서도 방문하지 않은 7을 방문한다.

  7에서는 6을 방문하는데 6의 경우는 연결된 노드가 없기 때문에 스택에서 제거하고 7에 연결된 8을 방문한다.

  8은 1과 연결되어있으며 방문하지 않은 인접노드가 없기 때문에 8, 7, 2를 스택에서 제거한다.

  1에서 방문하지 않은 인접노드인 3을 방문하고 3은 4를 4는 5를 방문한 뒤 더이상 방문할 인접노드가 없기에

  스택을 비워준다.

 

  스택 형태로 보면 다음과 같다.

1 2      
1 2 7    
1 2 7 6  
1 2 7 8  
1 3      
1 3 4    
1 3 4 5  
         

 

  방문순서를 정리해보면 1 2 7 6 8 3 4 5순서이다.

 

  스택자료구조에 기초한다는 점에서 구현이 간단하지만 실제로는 스택을 쓰지않아도 재귀함수를 이용하면 기본적으로

  스택과 비슷하게 동작한다는 점에서 재귀함수로 간단히 구현할 수 있으며 탐색을 수행함에 있어서 O(N)의 시간이

  소요되는 전수탐색 알고리즘이다.

 

import java.io.*;
import java.util.*;

public class Graph{
  private int V;//노드의 개수
  private LinkedList<Integer> adj[];//인접리스트
  
  Graph(int v){
    V = v;
    adj = new LinkedList[v];
    
    for(int i = 0; i < v; ++i){//인접리스트 초기화
      adj[i] = new LinkedList();
    }
  }
  
  void addEdge(int v, int w){
    adj[v].add(w);
    
    /*
      연결된 리스트 확인
      for(int i = 0; i < adj.length; i++){
        System.out.println("adj["+i+"] : "+adj[i]);
      }
    */
  }
  
  void DFSUtil(int v, boolean visited[]){
    //현재 노드를 방문한것으로 표시하고 값을 출력.
    visited[v] = true;
    System.out.print(v + " ");
    
    //방문한 노드와 인접한 모든 노드를 가져온다.
    Iterator<Integer> i = adj[v].listInteger();
    while(i.hasNext()){
      int n = i.next();
      /*
        다음값인 n의 값 확인
        System.out.println("i.next : "+n);
      */
      
      //방문하지 않은 노드면 해당 노드를 시작 노드로 다시 DFSUtil 호출
      if(!visited[n]){
        DFSUtil(n, visited);
      }
    }
  }
  
  void DFS(int v){
    //노드의 방문여부 판단 (초기값 : false)
    boolean visited[] = new boolean[V];
    
    //v를 시작노드로 DFSUtil 순환 호출
    DFSUtil(v, visited);
  }
  
  /*
  비 연결형 그래프일 경우
  void DFS(){
    //노드의 방문여부 판단
    boolean visited[] = new boolean[V];
    
    //모든 정점을 하나씩 방문
    for(int i = 0; i < V; ++i){
      if(visited[i] == false){
        DFSUtil(i, visited);
      }
    }
  }*/
  
  public static void main(String[] args){
    Graph g = new Graph(4);
    g.addEdge(0, 1);
    g.addEdge(0, 2);
    g.addEdge(1, 2);
    g.addEdge(2, 0);
    g.addEdge(2, 3);
    g.addEdge(3, 3);
    
    g.DFS(2);
    //DFS(); 비연결형그래프
  }
}

  강의에서 본 C예제코드는 이해가 많이 떨어져서 java코드로 예제를 찾아 학습했다.

  위 코드의 결과값은 2 0 1 3이 출력된다.

  스택을 이용한 구현이 아닌 재귀함수를 이용한 구현이다.

 

  addEdge는 노드를 연결해주는 것인데 다 연결되고나면

  adj[0] = [1, 2]

  adj[1] = [2]

  adj[2] = [0, 3]

  adj[3] = [3]

  이런 형태가 된다.

 

  그림으로 본다면

 

  이렇게 연결된 그래프형태다.

 

  연결까지 다 처리되고 나면 DFS로 시작노드를 넘겨주면서 처리하게 되는데 visited[]는 boolean형태로 V만큼의 크기를

  설정함으로써 adj배열과 같은 크기로 설정해준다.

  이때 모든 기본값은 false가 된다.

 

  DFSUtil에서는 v와 visited를 넘겨 처리해준다.

  처음 넘겨주는 노드는 2로 임의로 정한건데 visited[2] = true를 해줌으로써 2번 노드는 방문했다는것을 확인할 수

  있도록 한다.

  방문한 노드는 우선적으로 출력을 해주고 Iterator로 방문한 노드와 인접한 모든 노드를 가져온다.

  이 경우 adj[2] = [0, 3]이기 때문에 0과 3을 가져온다.

  연결되어있는 다음노드가 존재한다면 반복문을 실행하고 n에 next값을 넣어준다.

 

  그럼 n은 0이 되고 visited[0]은 아직 방문하지 않은 false상태이기 때문에 DFSUtil함수를 호출하게 된다.

  이렇게 계속 재귀적으로 반복하며 모든 노드를 방문하도록 수행한다.

 

 

레퍼런스

● 패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

gmlwjd9405.github.io/2018/08/14/algorithm-dfs.html

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

이진탐색트리(Binary Search Tree)  (0) 2021.01.16
너비우선탐색(BFS)  (0) 2021.01.15
그래프(Graph)  (0) 2021.01.13
순차탐색, 이진탐색  (0) 2021.01.12
우선순위 큐  (0) 2021.01.11

그래프

  그래프란 사물을 정점(vertex)와 간선(Edge)으로 나타내기 위한 도구이다.

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

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

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

 

  노드를 정점 혹은 노드라고 부르고 그 노드들을 이어주는 선을 간선이라고 한다.

  위 그림에서 0, 1, 2는 노드 3, 7은 간선이다.

 

  인접행렬에서 그래프는 2차원 배열로 표현한다.

  표를 보면 0에서 0으로 이동하는것은 이동하지 않기 때문에 0인것이고

  0에서 1로 이동하는 것은 3, 2로 이동하는것은 7 이런 형태이다.

  즉, 이동하는 간선이 3, 7인 것이다.

  무한이라고 되어있는 경우는 간선이 없는 경우이다.

  1에서 2로 가는 경우나 2에서 1로 가는경우는 간선이 없기 때문에 무한이다.

  그래프는 이런 2차원 배열 형태로 만든다.

 

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

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

  모든 간선에 가중치가 없는 그래프를 비가중치 그래프라고 한다.

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

  방향성을 가지지 않고 연결만 되어있는 그래프도 무방향 그래프라고 한다.

 

예제코드

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.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");
}

  5 3

  1 2

  3 4

  4 5

  이렇게 입력하면

  0 1 0 0 0

  1 0 0 0 0

  0 0 0 1 0

  0 0 1 0 1

  0 0 0 1 0

  이렇게 출력된다.

 

  a[x][y] = 1

  a[y][x] = 1

  이 부분을 처리할 때 보면 x와 y는 입력값으로 처음에 1와 2이 들어간다.

  n은 정점, m은 간선으로 5와 3이 처음에 입력되고 x와 y는 그 다음 입력값부터 들어간다.

  a[1][2] = 1,  a[2][1] = 1 이 형태가 되는건데

  이차원 배열이므로 첫행 2번째 인덱스에 1, 두번째행 1번째 인덱스에 1이 들어간다.

  출력문에서 보면 a[1][2] = 1 이   0 1 0 0 0 으로 출력된것을 확인 할 수 있다.

  출력하는 반복문을 0부터가 아닌 1부터 시작했기 때문에 0번째 인덱스는 제외하고 1번 인덱스부터 출력이 되서

  보이는 숫자 그대로의 위치로 확인한다.

  4번행은 1이 두번들어가있는데 3 4로 한번 4 5로 한번 들어갔기 때문에 두개의 인덱스에 1이 들어간것이다.

 

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

  모든 간선이 방향을 갖는 그래프를 방향그래프라고 한다.

  모든 간선에 가중치가 있는 그래프를 가중치그래프라고한다.

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

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
//스택기반 연결리스트

typedef struct{
  int index;
  int distance;
  struct Node *next;
} Node;
//한개의 노드 정의
//어떤 인덱스로 어떠한 만큼의 가중치
//즉, 거리를 갖는지 정의할 수 있도록 한다.
//연결리스트니까 next로 다음 노드를 기록할 수 있도록 한다.

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;
    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;
}
/*
  노드의 개수를 n, 간선의 개수를 m에 입력받는다.
  인접리스트로 구현할 때는 노드의 개수만큼 연결리스트가 필요하기 때문에
  노드의 개수에 +1을 더한만큼 동적할당을 해준다.
  여기서 출력문을 만들 때 0부터가 아닌 1부터 출력하도록 할것이기 때문이다.
  0부터 출력한다면 더하지 않아도 되는 부분이다.
  
  첫 반복문에서 각 노드를 동적할당으로 초기화
  next에는 NULL을 넣어서 root가 NULL값을 가리키도록 한다.
  
  두번째 반복문은 각 간선의 정보를 사용자로부터 입력받도록 한다.
  입력받은 값을 바로 리스트화 하기 위해 addFront로 보내 처리한다.
  
  마지막 반복문에서는 각 원소를 출력하고
  showAll에서 특정한 노드 정점에 연결되어있는 다른 정점을 출력하도록 한다.
*/

  5 4

  1 2 9

  1 3 8

  1 5 10

  3 4 8

  이렇게 입력해주면

 

  원소 [1] : 5(거리 : 10) 3(거리 : 8) 2(거리 : 9)

  원소 [2] :

  원소 [3] : 4(거리 : 8)

  원소 [4] :

  원소 [5] :

 

  이렇게 출력된다.

 

  5는 노드의 개수 4는 간선의 개수이다.

  1 2 9라는 것은 1이라는 노드와 2라는 노드의 거리가 9라는 것이다.

  1 2 9를 기준으로 보자면 입력받고나서 addFront(a[1], 2, 9) 형태로 넘어간다.

 

  그럼 addFront에서는 root에 a[1], index에 2, distance에 9가 들어가게되고

  새로 할당받은 node의 index는 2, distance는 9, next는 a[1]의 next니까 NULL root의 next는 node가 된다.

 

void addFront(Node *root, int index, int distance){//a[1], 2, 9
  Node *node = (Node*)malloc(sizeof(Node));
  node->index = index = 2;
  node->distance = distance = 9;
  node->next = root->next = NULL;
  root->next = node = index(2), distance(9);
}


void addFront(Node *root, int index, int distance){ //a[1], 3, 8
  Node *node = (Node*)malloc(sizeof(Node));
  node->index = index = 3;
  node->distance = distance = 8;
  node->next = root->next = node(index(2), distance(9));
  root->next = node = index(3), distance(8);
}

  그림으로 보면 다음과 같다.

 

 

  제일먼저 n의 크기를 갖도록 만들어주고

  addFront에서 node를 만들어 index과 distance를 정의한다.

  그리고 연결해주는 것이다.

 

 

  입력 순으로 이렇게 모두 연결된 뒤 출력하게 된다.

 

  인접행렬은 모든 정점들의 연결 여부를 저장하여 O(V^2)의 공간을 요구하므로 공간 효율성이 떨어지지만

  두 정점이 서로 연결되어 있는지 확인하기 위해 O(1)의 시간을 요구한다.

  인접리스트는 연결된 간선의 정보만을 저장하여 O(E)의 공간을 요구하므로 공간 효율성이 우수하지만

  두 정점이 서로 연결되어있는지 확인하기 위해 O(V)의 시간을 요구한다.

 

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

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

너비우선탐색(BFS)  (0) 2021.01.15
깊이우선탐색(DFS)  (0) 2021.01.14
순차탐색, 이진탐색  (0) 2021.01.12
우선순위 큐  (0) 2021.01.11
트리(Tree)  (0) 2021.01.10

순차탐색(Sequentail Search)

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

  아래의 배열에서 '배열'이라는 문자열을 찾는다고 하면

인덱스

0

1

2

3

4

원소

순차

탐색

정렬

배열

리스트

  앞에서부터 문자열을 순차적으로 탐색해서 찾은 문자열의 인덱스를 반환한다.

 

예제코드

 

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

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*) * n);
    scanf("%s", array[i]);
  }
  
  for(int i = 0; i < n; i++){
    if(!strcmp(array[i], word)){
      printf("%d번째 원소입니다.", i + 1);
      founded = 1;
    }
  }
  
  if(!founded) printf("원소를 찾을 수 없습니다.\n");
  system("pause");
  return 0;
}

  5 배열

  순차 탐색 정렬 배열 리스트

  이렇게 입력하면 '4번째 원소입니다' 라는 결과값이 출력된다.

 

  n은 배열의 크기, word는 문자열이다.

  첫번째 반복문으로 배열의 원소값을 입력하고 두번째 반복문에서 값을 찾는다.

  word를 기준으로 배열의 인덱스를 하나씩 증가시키며 찾아나가고 strcmp함수로 인덱스의 원소값, 그리고 찾고자하는

  문자열인 word를 비교해서 같다면 해당 인덱스값 + 1을 해서 출력을 해준다.

  +1을 한 값으로 출력하는것은 인덱스 번호는 0부터 시작하기 때문에 몇번째에 있는지 출력하기 위해 더했다.

 

  순차탐색은 데이터 정렬 유무에 상관없이 가장 앞에 있는 원소부터 하나씩 확인해야 한다는 점에서 O(N)의

  시간 복잡도를 갖는다.

 

이진탐색(Binary Search)

  이진탐색은 배열 내부 데이터가 이미 정렬되어 있는 상황에서만 사용가능한 알고리즘이다.

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

 

  이진 탐색의 탐색과정은 다음과 같다.

  찾을 원소값이 37이고

인덱스 0 1 2 3 4 5 6 7 8
원소 15 27 37 46 57 69 73 85 98

  탐색할 배열이 이렇게 정렬되어 있을 때 중앙의 인덱스 원소값을 기준으로 먼저 탐색한다.

  그럼 가운데 인덱스는 4고 원소값은 57인데 찾을 원소보다 크기 때문에 왼쪽으로 탐색을 하게 된다.

인덱스 0 1 2 3 4 5 6 7 8
원소 15 27 37 46 57 69 73 85 98

  왼쪽에 남은 인덱스인 0 1 2 3 중에서 중간값인 27을 기준으로 한번 더 체크하면 27이 더 작기 때문에

  오른쪽으로 탐색하고 바로 오른쪽 인덱스인 2번인덱스를 탐색하여 체크한다.

  

  이렇게 모든 인덱스를 탐색하지 않아도 되기 때문에 순차탐색보다 더 빠르게 찾을 수 있다.

  

  이진 탐색은 한번 확인할 때마다 봐야하는 원소의 개수가 절반씩 줄어든다는 점에서 탐색시간이 O(logN)의

  시간 복잡도를 갖는다.

 

  start, mid, end로 정의해서 mid위치에 있는 원소와 반복적으로 비교하게 된다.

 

예제코드

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.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;//mid값과 target값이 일치한다면 mid값을 반환
  else if(a[mid] > target) return search(start, mid - 1, target);//mid값이 target보다 크다면
  else return search(mid + 1, end, target);//mid값이 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;
}

  n은 배열의 크기 target은 찾고자하는 값이다.

  첫 반복문에서 배열의 원소값들을 입력해주는데 이때 원소값들은 정렬된 상태로 입력해줘야 한다.

  result는 search함수에서 처리하고 난 반환값이다.

  첫 시작은 0번인덱스가 되어야 하고 마지막 인덱스는 배열 크기 - 1이고 찾는 값을 입력한 값을 넘겨준다.

 

  search에서는 제일먼저 start와 end를 비교해서 start가 더 크다면 -9999를 반환하도록 한다.

  처음부터 이 조건문에 걸릴일은 없겠지만 처리과정을 반복하면서 찾고자하는 값이 배열에 없으면 더 진행하지않고

  반환하기 위해서 맨 위에 작성한다.

 

  mid는 가운데 인덱스 값을 찾기 위해 start와 end를 더한 값을 2로 나눠준다.

 

  a배열의 가운데 인덱스인 mid번째 인덱스의 원소값이 찾고자하는 값인 target과 같다면 더 처리할 필요가 없으므로

  인덱스 값인 mid를 반환한다.

 

  a[mid]가 target보다 크다면 왼쪽에서 탐색해야 하므로 search함수로 값을 다시 넘긴다.

  start는 그대로 0이어야 하니까 start로, end는 mid보다 하나 작은 인덱스까지만 탐색해야 하니까 mid - 1을 해준다.

 

  a[mid]가 target보다 작다면 오른쪽에서 탐색해야 하므로 start는 mid보다 하나 더 큰 값으로, end는 그대로 end를 

  넘겨 처리한다.

 

  출력은 result가 원소값이 없을때의 반환값인 -9999가 아니라면 몇번째 원소인지 출력해준다.

  이때 result + 1을 해준 이유는 배열은 0부터 시작하니까 1부터 시작하도록 만들어서 출력하기 위해서다.

  

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

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

깊이우선탐색(DFS)  (0) 2021.01.14
그래프(Graph)  (0) 2021.01.13
우선순위 큐  (0) 2021.01.11
트리(Tree)  (0) 2021.01.10
계수정렬, 기수정렬  (0) 2021.01.09

우선순위 큐

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

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

 

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

  

  일반적인 형태의 큐는 선형적인 형태를 갖고 있지만 우선순위 큐는 트리(Tree)구조로 보는 것이 합리적이다.

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

 

최대힙(Max Heap)

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

  완전 이진트리는 자식노드가 2개씩 있거나 아예 없어야 한다.

 

  위 두 트리는 완전이진트리가 아니다.

  왼쪽 트리의 경우는 위에서 얘기했듯이 7의 자식노드가 하나밖에 없기 때문에 완전이진트리가 성립하지않고

  오른쪽 트리의 경우는 자식노드가 부모노드보다 크기때문에 성립하지 않는다.

  이렇게 완전이진트리가 아니라면 최대힙에 해당하지 않는다.

 

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

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

  큐를 기반으로 하기 때문에 enQueue, deQueue로 정의한다.

  enQueue는 우선순위 큐에 데이터를 삽입.

  deQueue는 우선순위 큐에서 데이터를 추출하는 것이다.

 

우선순위 큐의 삽입

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

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

 

  위와 같은 처리과정으로 9를 상위노드와 비교해서 올려줌으로써 최대힙을 유지하도록 한다.

 

우선순위 큐의 삭제

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

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

 

    루트노드를 삭제한 뒤 가장 하위 노드를 루트로 올려준다.

  루트노드의 하위노드중 더 큰값과 위치를 바꿔준다.

  바꿔준다음 또 그 하위 노드와 비교한다. 이 경우 1과 4를 하위노드로 갖기 때문에 더이상 내려가지 않는다.

 

예제코드

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.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 enQueue(priorityQueue *pq, int data){
  if(pq->count >= MAX_SIZE) return;
  pq->heap[pq->count] = data;
  //count는 큐에 담겨있는 원소의 개수
  //제일 뒤로 데이터가 들어간다.
  
  int now = pq->count;
  //삽입된 데이터에 해당하는 인덱스를 now라고 정의.
  
  int parent = (pq->count - 1) / 2;
  //now에서 1을 빼고 2로 나누어진 값이 부모노드다.
  //3개층이 있는 완전 이진트리로 가정하면
  //오른쪽 최하단에 새로 들어오는 노드의 인덱스 6이다.
  //최하단 노드는 4개. 3 4 5 6 이기 때문에 
  //새로들어오는 노드의 부모노드 인덱스는 2가된다.
  //(6 - 1)/2 = 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++;
  //값이 새로 들어왔으니 count를 ++해준다.
}

int deQueue(priorityQueue *pq){
  //pq라는 매개변수로 큐를 받아서 처리
  
  if(pq->count <= 0) return -9999;
  
  int res = pq->heap[0];
  //루트노드를 담는다
  
  pq->count--;
  //삭제하는것이니 count를 1빼준다.
  
  pq->heap[0] = pq->heap[pq->count];
  //가장 마지막 노드를 루트노드에 넣어준다.
  
  int now = 0, leftChild = 1, rightChild = 2;
  int target = now;
  //target은 자기자신을 가리키도록 한다.
  //새 원소를 추출한 이후에 하향식으로 힙을 구성
  
  while(leftChild < pq->count){
    //leftChild가 카운트보다 작을때만 반복한다.
    //원소가 존재할때까지만 반복한다는 것이다.
    
    if(pq->heap[target] < pq->heap[leftChild]) target = leftChild;
    //target이 0이므로 root노드가 왼족 자식노드보다
    //작다면 둘을 바꿔주는 것이다.
    
    if(pq->heap[target] < pq->heap[rightChild] && rightChild < pq->count) target = rightChild;
    //루트노드가 오른쪽 자식노드보다 작고 오른쪽 자식노드가 카운트 보다 작다면
    //target을 rightChild로 바꾼다.
    //count보다 작다는 조건을 넣어놓는 이유는 인덱스를 벗어나는 경우를 방지하기 위해서다.
    //rightChild가 원소 개수보다 작을때에 한해서만 검사하도록 한다.
    
    if(target == now) break;
    //만약 두 자식노드보다 자기자신의 값이 더 크다면 이라는 조건이다.
    //target이 now가 된다는것은 위 조건문에서 false로 아무처리 되지 않고 그냥 내려왔기 때문에
    //가능한 것이므로 더이상 내려가지 않아도 된다는 것이다.
    //그럼 break로 빠져나가도록 한다.
    
    else{
      swap(&pq->heap[now], &pq->heap[target]);
      //자식노드가 더 크다면 변경해줘야 한다.
      //예를들어 leftChild가 더 큰 값이라 바꿔줘야 한다면
      //위 leftChild와 비교한 조건문에서 이미 target은 leftChild가 되었을 것이고
      //swap에서는 a와 b를 바꿔주므로 root인 now와 target인 leftChild의 순서가 바뀌게 된다.
      
      now = target;
      //아래로 더 체크해야 하기 때문에 now를 target으로 바꿔준다.
      
      leftChild = now * 2 + 1;
      rightChild = now * 2 + 2;
      //자리바꿈을 한 후에는 그 아래 노드들도 체크해야 하기 때문에 
      //now뿐만 아니라 두 자식노드의 값도 바꿔줘야 한다.
      //left로 내려왔다고 가정하고 보자면
      //now는 루트의 leftChild인 1이되고
      //leftChild는 1 * 2 + 1 = 3
      //rightChild는 1 * 2 + 2 = 4
      //이렇게 처리되어 1노드의 두 자식노드 인덱스가 된다.
    }
  }
  return res;
}

int main(void){
  int n, data;
  scanf("%d", &n);
  priorityQueue pq;
  pq.count = 0;
  //count를 0으로 초기화
  
  for(int i = 0; i < n; i++){
    scanf("%d", &data);
    enQueue(&pq, data);
  }
  
  for(int i = 0; i < n; i++){
    int data = deQueue(&pq);
    printf("%d ", data);
  }
  system("pause");
  return 0;
}

  위치를 잡고 설명하기 애매해서 대체로 주석처리했다.

  

  n은 데이터의 개수를 입력하는 것이고

  data에 값을 입력해서 넣어주면 큰수부터 작은수로 출력되는것을 확인할 수 있다.

  큰수부터 작은수로 출력되는것은 data = deQueue(&pq)인데 deQueue에서 반환되는 값은 res이고

  res는 heap[0]이기 때문에 가장 큰수부터 차례로 출력이 된다.

  가장 큰 수 부터 차례로 출력이 된다는 것은 제대로 처리가 되고 있다고 볼 수 있다.

 

  강의 볼 때는 몰랐었는데 정리하면서 출력이 이렇게 되는걸 늦게 알아서...

  한번만 추출하고 출력하거나 한번 반복할때마다 전체 출력하는 부분을 추가해봐야겠다.

 

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

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

그래프(Graph)  (0) 2021.01.13
순차탐색, 이진탐색  (0) 2021.01.12
트리(Tree)  (0) 2021.01.10
계수정렬, 기수정렬  (0) 2021.01.09
퀵정렬  (0) 2021.01.07

+ Recent posts