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

  1. 2018.09.01 자료구조 이진 탐색 트리(Binary Search Tree)
  2. 2018.08.30 자료구조 트리(Tree)
  3. 2018.08.19 [자료구조] 큐 QUEUE
  4. 2018.08.19 [자료구조] 스택 STACK
  5. 2018.08.17 [BOJ] 1912 연속합 - 동적계획법 풀이


이전 시간에 트리에 대해 공부했습니다. (참고 : 자료구조 트리)

이번 시간에는 이진 탐색 트리에 대해 공부해보겠습니다.


♪ 이진 탐색 트리(Binary Search Tree) : 노드기반(Node-Based) 이진 트리 자료구조입니다.


♪ 특징(Properties) 

좌측 하위 트리(Left Subtree)의 노드들은 상위 노드보다 작거나 같은 값입니다.

우측 하위 트리(Right Subtree)의 노드들은 상위 노드보다 큰 값입니다.

좌측 및 우측 하위트리 역시 이진 탐색 트리입니다. (하위트리의 하위트리들도 모두 위 특징에 해당합니다)


♪ 탐색(Search) 

☞ 탐색의 시작은 루트 노드(Root Node)에서 시작합니다. 만약 탐색하려는 값이 루트 노드의 값이라면 루트 노드의 값을 반환합니다.

☞ 만약 탐색하려는 값이 루트 노드의 값보다 작다면 좌측 하위 트리로 이동해서 탐색을 반복합니다.

☞ 만약 탐색하려는 값이 루트 노드의 값보다 크다면 우측 하위 트리로 이동해서 탐색을 반복합니다.


# 탐색 - 소스코드(C/C++)

struct node* search(struct node* root, int key) // 키 값 : 탐색하려는 값

{

if (root == NULL || root->key == key) // 루트 노드가 없거나 루트 노드의 값이 키 값인 경우

return root; // 루트 노드 반환

    

if (root->key < key) // 키 값이 루트 노드의 값보다 크다면

return search(root->right, key); // 우측 하위 트리 탐색

 

// 키 값이 더 작다면

return search(root->left, key); // 좌측 하위 트리 탐색

}



♪ 삽입(Insertion) : 새로운 노드는 항상 잎(Leaf) 노드에서 발생합니다.

☞ 루트 노드에서 탐색을 시작합니다. 만약 루트 노드가 없다면 노드를 삽입합니다.

☞ 잎(Leaf) 노드를 만날 때 까지 탐색을 시작합니다.

☞ 잎(Leaf) 노드를 만났다면 새로운 노드를 삽입합니다.(새 노드 값이 작다면 좌측, 크다면 우측)


# 삽입 - 소스코드(C/C++)

struct node *newNode(int item) // 새로운 노드 생성 함수

{

    struct node *temp =  (struct node *)malloc(sizeof(struct node)); // 새로운 노드를 동적할당

    temp->key = item; // 키 값을 설정

    temp->left = temp->right = NULL; // 좌측 및 우측 노드 null로 초기화

    return temp;

}

  

struct node* insert(struct node* node, int key) // 삽입 함수; node는 root노드, key는 삽입하려는 값

{

    if (node == NULL) return newNode(key); // 루트 노드가 없다면 바로 삽입

 

    if (key < node->key) // 새 노드가 루트노드 값보다 작다면

        node->left  = insert(node->left, key); // 좌측 서브트리로 이동해서 반복

    else if (key > node->key) // 새 노드가 루트노드 값보다 크다면

        node->right = insert(node->right, key); // 우측 서브트리로 이동해서 반복

 

    return node;

}



♪ 삭제(Delete) :  설명은 소스코드 주석으로 대체합니다.


# 삭제 - 소스코드(C/C++)

struct node * minValueNode(struct node* node) // 가장 작은 노드 반환 함수

{

    struct node* current = node; // 루트 노드 복사

 

    while (current->left != NULL) // 좌측 서브트리가 없을 때 까지 반복

        current = current->left;

 

    return current; 

}

 

struct node* deleteNode(struct node* root, int key) // 삭제 함수

{

    if (root == NULL) return root; // 루트 노드 null이라면 루트 노드 반환

 

    if (key < root->key) // 삭제하려는 노드의 값이 루트노드의 값보다 작다면

        root->left = deleteNode(root->left, key); // 좌측 서브트리로 이동해서 반복

 

    else if (key > root->key) // 크다면

        root->right = deleteNode(root->right, key); // 우측 서브트리로 이동해서 반복


    else // 삭제하려는 노드가 루트 노드

    {

        if (root->left == NULL) // 좌측 서브트리가 없는 경우

        {

            struct node *temp = root->right; // 우측 서브트리 복사

            free(root); // 루트 노드 삭제

            return temp; // 우측 서브트리 반환

        }

        else if (root->right == NULL) // 우측 서브트리가 없는 경우

        {

            struct node *temp = root->left; // 좌측 서브트리 복사

            free(root); // 루트 노드 삭제

            return temp; // 좌측 서브트리 반환

        }

 

        // 좌, 우측 서브트리 모두 있을 경우

        struct node* temp = minValueNode(root->right); // 우측 서브트리 중 가장 작은 값 복사

 

        root->key = temp->key; // 복사 한 값을 루트 노드에 복사

 

        root->right = deleteNode(root->right, temp->key); // 루트의 우측 서브트리 이동해서 반복

    }

    return root;

}





정보가 유익하셨다면 아래 공감버튼 눌러주시면 감사하겠습니다.

질문사항은 댓글로 달아주시면 성의껏 답변해드리겠습니다.


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

정렬 알고리즘 종류  (0) 2018.09.12
검색 알고리즘 종류  (2) 2018.09.11
자료구조 그래프(Graph)  (1) 2018.09.06
자료구조 힙(Heap)  (0) 2018.09.05
자료구조 트리(Tree)  (0) 2018.08.30
[자료구조] 큐 QUEUE  (0) 2018.08.19
[자료구조] 스택 STACK  (0) 2018.08.19
[BOJ] 1912 연속합 - 동적계획법 풀이  (0) 2018.08.17


◎ 트리(Tree) : 배열(Array), 링크드 리스트(Linked List), 스택(Stack), 큐(Queue) 같이 선형 자료구조(Linear Data Structure)와 달리 트리는 계층적 자료구조(Hierarchical Data Structure) 입니다.


용어(Tree Vocabulary)

- 노드(Node) : 트리 구조의 각각의 요소(Element)

- 루트(Root) 노드 : 최상위(Topmost) 노드

- 자식(Child) 노드 : 해당 노드 바로 아래 노드

- 부모(Parent) 노드 : 해당 노드 바로 위 노드

- 잎(Leaf) 노드 : 자식 노드가 없는 노드



사용 목적

1. 컴퓨터 파일 시스템과 같이 계층을 형성하는 정보를 저장하기 위해 사용합니다.

2. 이진 탐색 트리와 같은 정렬되어있는 트리는 적당한(Moderate) 접근, 탐색을 제공합니다. (링크드 리스트보다 빠르지만 배열보다 느림)

3. 트리는 적당한(Moderate) 입력/삭제를 제공합니다. (배열보다 빠르지만 정렬되지 않은 링크드 리스트보다 느립니다.)

4. 링크드 리스트와 마찬가지로 배열과 달리 트리는 노드들이 포인터로 연결되어있어 노드수의 상한선(Upper limit)이 없습니다.


트리 응용(Applications)

1. 계층적 데이터를 다룹니다.(Manipulate)

2. 정보를 검색하기 쉽게 만듭니다. - 트리 순회(Tree Traversal) 참조

3. 정렬된(Sorted) 리스트 데이터를 다룹니다.

4. 라우터 알고리즘(Router Algorithms)


이진 트리(Binary Tree) : 자식 노드의 수가 2개 이하인 트리를 이진 트리라고 합니다. 이진 트리 각각의 노드들은 오직 2개의 자식만 가질 수 있으며, 이를 일반적으로(typically) 왼쪽 자식, 오른쪽 자식이라고 부릅니다.


- C에서 이진 트리 표현(Binary Tree Representation in C) : 트리에서 최상위 노드는 포인터로 표현됩니다. 트리가 비어있다면(empty) 루트의 값은 NULL입니다.

- C에서 트리 노드는 구조체로 표현합니다. 아래 예제는 integer형 자료를 저장하는 트리 노드의 예제입니다.


struct node  // 노드 구조체

{

int data; // 데이터 값

struct node *left; // 왼쪽 자식

struct node *right; // 오른쪽 자식

};


- C에서 4개의 노드를 생성하는 간단한 예제입니다.


struct node* newNode(int data) // 노드 생성

{

struct node* node = (struct node*) malloc(sizeof(struct node)); // node 구조체를 동적 생성


node -> data = data; // 입력 한 값을 저장 

node -> left = NULL; // 왼쪽 자식 : 없음

node -> right = NULL; // 오른쪽 자식 : 없음


return node; // 노드 반환

}


int main()

{

struct node* root = new Node(1); // 노드 생성(루트 노드, 값 = 1)


root -> left = newNode(2); // 노드 생성(왼쪽 자식, 값 = 2)

root -> right = newNode(3); // 노드 생성(오른쪽 자식, 값 = 3)

root -> left -> left = newNode(4); // 노드 생성(왼쪽 자식의 왼쪽 자식, 값 = 4)


return 0;

}


위의 소스코드를 그림으로 표현하면 다음과 같습니다.



정보가 유익하셨다면 아래 공감버튼 눌러주시면 감사하겠습니다.

질문사항은 댓글로 달아주시면 성의껏 답변해드리겠습니다.





1. 의미
- 큐(Queue)의 사전적 의미는 무엇을 기다리기 위해 서는 줄, 대기열입니다.
예를들어 우리가 게임을 할 때 '큐를 잡는다, 큐를 기다린다'라는 말의 큐는 바로 줄, 대기열을 의미합니다. 우리가 줄을 서면 가장 앞에있는 사람(가장 먼저 들어온 데이터)이 가장 먼저 들어가죠? 이러한 구조를 선입선출(First In First Out; FIFO)구조라고 합니다. 

2. 연산
- 일반적으로 큐 자료구조의 앞을 Front, 뒤를 Rear라고 칭합니다. 그림으로 표현하면 다음과 같습니다.


큐가 비어있는 상태를 front == rear(같음) 상태로 표현합니다.
(초기화시 front = rear = 0으로 초기화)



삽입(Enqueue)연산은 rear의 위치에 데이터를 삽입하고 rear의 위치를 1 증가 시켜줍니다.



삭제(Dequeue)연산은 front 위치의 데이터를 반환하고 front의 위치를 1 증가 시켜줍니다.
이러면 front와 rear가 같은 값이 되므로 다시 큐가 비어있는 상태가 됩니다.



큐의 끝(rear)가 배열의 마지막을 가리키고있는 상태가 큐가 가득 찬 상태입니다.
이렇게 큐를 배열로 구현했을 경우 0번 인덱스는 사용하지 않는 메모리가 되었습니다.
물론 고정적인 배열의 크기도 문제가 됩니다.
이를 해결하기 위해 원형 큐, 링크드 리스트로 구현한 큐... 등 여러가지 큐가 존재합니다.

3. 사용
프로세스 관리
운영체제(OS)의 작업 큐
프린터 출력의 문서 대기...등 
입력된 시간 순서대로 처리해야하는 모든 상황에 거의 큐가 사용됩니다.

4. 구현
배열로 구현한 큐)
#include <iostream> 
using namespace std;

class queue
{
private:
     int *queue_arr; // 큐 배열(동적 할당을 위해 포인터로 선언) 
     int queue_size; // 큐의 크기 
     int 
front, rear;

public:
     queue() // 디폴트 생성자 함수 
     {
          
front = 0; rear = 0// front와 rear 위치를 0으로 초기화
          queue_size = 5// 매개변수 없이 객체 생성 시 스택 크기 5로 지정 
          queue_arr = new int[queue_size]; // 배열 동적 할당 
          for (int i = 0; i < queue_size; i++)
          {
               queue_arr[i] = 0// 큐의 배열은 모두 0으로 초기화 
          }
     }
     queue(int 
size// 생성자 함수 
     {
          front = 0; rear = 0// front와 rear 위치를 0으로 초기화
          queue_size = 
size// 입력한 매개변수 크기만큼 스택 크기 지정 
          queue_arr = new int[queue_size]; // 배열 동적 할당 
          for (int i = 0; i < queue_size; i++)
          {
               queue_arr[i] = 0// 큐의 배열은 모두 0으로 초기화 
          }
     }
     ~queue() // 소멸자 함수 
     {
          delete[] queue_arr;
     }
     void enqueue(int data) // 삽입 함수 
     {
          if (queue_is_full()) // 큐가 가득 차 있는 경우 
          {
               cout << "큐가 가득 차 있습니다." << "\n";
               return// 함수 종료 
          }
          else // 아닐 경우
          {
               queue_arr[rear++= data;
               // 큐 배열 rear 위치의 데이터를 삽입 후 rear의 크기 1 증가
               // 위 코드가 이해가지 않는다면 후위 증감 연산자에 대해 공부할 것
               cout << "데이터 삽입 성공!" << "\n";
          }
     }
     void dequeue() // 삭제 함수 
     {
          if (queue_is_empty()) // 큐가 비어있는 경우 
          {
               cout << "큐가 비어있습니다." << "\n";
               return// 함수 종료 
          }
          else // 아닐 경우 
          {
               cout << "반환 된 데이터 : " << queue_arr[front<< "\n"// 데이터 반환 
               queue_arr[front++= 0
               // 큐 배열 front 위치의 데이터를 삭제 후 front 크기 1 증가
          }
     }
     bool queue_is_empty() // 큐가 비어있는지 확인 함수 
     {
          if (front == rear) // front와 rear가 같은 위치라면
               return true;
          else // 아닐 경우 비어있지 않음 
          return false;
     }
     bool queue_is_full() // 큐가 가득 차 있는지 확인 함수 
     {
          if (rear == queue_size - 1// rear가 큐 배열의 마지막 위치라면
               return true;
          else // 아닐 경우 가득 차 있지 않음 
               return false;
     }
     void print_queue() // 큐 배열 전체 출력 함수 
     {
          cout << "Queue : [ ";
          for (int i = 0; i < queue_size; i++)
          {
               if (i != queue_size - 1)
               {
                    cout << queue_arr[i] << " , ";
               }
               else
               {
                    cout << queue_arr[i];
               }
          }
     cout << " ] " << "\n";
     cout << "front 위치 : " << front << "\n";
     cout << "rear 위치 : " << rear << "\n";
     }
};

int main()
{
     queue *que;
     int input_size;
     cout << "큐 배열 크기 입력 : ";
     cin >> input_size;
     if (input_size <= 0)
     {
          cout << "잘못 된 입력입니다. 디폴트 사이즈(5)로 생성합니다." << "\n";
          que = new queue();
     }
     else
     {
          que = new queue(input_size);
     }
     while (1)
     {
          int input;
          cout << "\n" << "[명령어 입력]" << "\n";
          cout << "1. ENQUEUE " << "\n" << "2. DEQUEUE " << "\n" << "3. PRINT " << "\n" << "4. EXIT : ";
          cin >> input;
          switch (input)
          {
          case 1:
               int input_data;
               cout << "삽입 할 데이터를 입력하세요 : ";
               cin >> input_data;
               que->enqueue(input_data);
               break;
          case 2:
               que->dequeue();
               break;
          case 3:
               que->print_queue();
               break;
          case 4:
               cout << "프로그램을 종료합니다." << "\n";
               return 0;
          default:
               cout << "잘못 된 입력입니다. 다시 입력해 주세요" << "\n";
          }
     }     
}

질문 사항은 댓글로 남겨주세요!



1. 의미
- 스택 자료구조는 아래가 막혀있는 긴 통에 물건을 층층이 쌓아둔 상태로 보시면 됩니다. 
예를들어 물건을 쌓기위해서는 현재 쌓여있는 물건 중 가장 위쪽에 쌓일 것이고, 꺼내기 위해서는 현재 쌓여있는 물건 중 가장 위쪽 물건이 꺼내질 것입니다. 이러한 구조를 선입후출(First In Last Out; FILO)또는 후입선출(Last In First Out; LIFO) 구조라고 칭합니다.

2. 연산
- 스택 자료구조의 연산(삽입, 삭제, 읽기)은 모두 스택의 꼭대기에서 일어납니다. 스택의 꼭대기를 가리키는 변수를 통상적으로 top이라고 합니다.


top이 -1을 가리키고 있는 상태가 스택이 비어있는 상태입니다.
(이해를 돕기위해 -1 인덱스를 표기한 것이지 배열의 인덱스는 0에서 시작합니다.)

삽입(push) 연산은 top이 가리키는 위치를 1 증가시키고, 그 위치에 데이터를 삽입합니다.


top이 인덱스 맨 끝을 가리키고 있는 상태가 스택이 가득 찬 상태입니다.
(인덱스 맨 끝은 스택 사이즈 -1입니다. 인덱스가 0부터 시작하기 때문)


삭제(pop) 연산은 top이 가리키는 위치의 데이터를 반환하고, 위치를 1 감소합니다.

3. 사용
- 스택은 컴퓨터 구조에서 다음과 같이 다양하게 사용됩니다. 

브라우저에서 이전페이지, 다음페이지 이동
에디터에서 실행취소, 다시실행
괄호검사
역순문자열 만들기
후위 표기법 수식의 연산 등...

아래의 구현방법을 통해 실제 스택 사용 예를 직접 구현해 보는것도 자료구조의 이해와 실력 향상에 도움이 될 것입니다.

4. 구현
C++ Class를 이용해 스택 자료구조를 배열로 구현했습니다.

#include <iostream> 
using namespace std; 

class stack 

private : 
     int *stack_arr; 
// 스택 배열(동적 할당을 위해 포인터로 선언) 
     int stack_size;
 // 스택의 크기 
     int top = -1; 
// top은 -1로 초기화  

public : 
     stack() 
// 디폴트 생성자 함수 
     
          stack_size = 5; 
// 매개변수 없이 객체 생성 시 스택 크기 5로 지정 
          stack_arr = new int[stack_size]; 
// 배열 동적 할당 
          for (int i = 0; i < stack_size; i++) 
          
               stack_arr[i] = 0;
 // 스택의 배열은 모두 0으로 초기화 
          
     
     stack(int size) 
// 생성자 함수 
     
          stack_size = size; 
// 입력한 매개변수 크기만큼 스택 크기 지정  
          stack_arr = new int[stack_size]; 
// 배열 동적 할당 
          for (int i = 0; i < stack_size; i++) 
          
               stack_arr[i] = 0; 
// 스택의 배열은 모두 0으로 초기화 
          
     
     ~stack() 
// 소멸자 함수 
     
          delete[] stack_arr; 
     
     void push(int data) 
// 삽입 함수 
     
          if (stack_is_full()) 
// 스택이 가득 차 있는 경우 
          
               cout << "스택이 가득 차 있습니다." << "\n"; 
               return;
 // 함수 종료 
          
          else 
// 아닐 경우 정상 작동 
          
               top++; 
// top 위치 1 증가 
               stack_arr[top] = data; // 데이터 삽입 
               cout << "데이터 삽입 성공!" << "\n"; 
          
     
     void pop() 
// 삭제 함수 
     
          if (stack_is_empty()) 
// 스택이 비어있는 경우 
          
               cout << "스택이 비어있습니다." << "\n"; 
               return; 
// 함수 종료 
          
          else 
// 아닐 경우 
          
               cout << "반환 된 데이터 : " << stack_arr[top] << "\n"; 
// 데이터 반환 
               stack_arr[top] = 0; 
// 데이터 삭제 
               top--; 
// top 위치 1 감소 
          
     
     int peak() 
// 현재 꼭대기 위치 확인 함수 
     
          return stack_arr[top]; 
     
     bool stack_is_empty()
 // 스택이 비어있는지 확인 함수 
     
          if (top == -1)
 // top이 -1을 가리키면 비어있음 
               return true;  
          else 
 // 아닐 경우 비어있지 않음 
               return false; 
     
     bool stack_is_full() 
// 스택이 가득 차 있는지 확인 함수 
     
          if (top == stack_size - 1) 
// top이 인덱스 마지막을 가리키면 가득 차 있음 
               return true; 
          else 
 // 아닐 경우 가득 차 있지 않음 
               return false; 
     
     void print_stack() 
// 스택 전체 출력 함수 
     
          cout << "STACK : [ "; 
          for (int i = 0; i < stack_size; i++) 
          
               if (i != stack_size - 1) 
               
                    cout << stack_arr[i] << " , "; 
               
          else 
          
               cout << stack_arr[i]; 
          
     
     cout << " ] " << "\n"; 
     cout << "top 위치 : " << top << "\n"; 
     
}; 

int main()  

     stack *stk; 
     int input_size; 
     cout << "스택 크기 입력 : "; 
     cin >> input_size; 
     if (input_size <= 0) 
     
          cout << "잘못 된 입력입니다. 디폴트 사이즈(5)로 생성합니다." << "\n"; 
          stk = new stack(); 
     
     else 
     
          stk = new stack(input_size); 
     
     while (1) 
     
          int input; 
          cout << "\n" << "[명령어 입력]" << "\n"; 
          cout << "1. PUSH " << "\n" << "2. POP " << "\n" << "3. PEAK "  
          << "\n" << "4. PRINT " << "\n" << "5. EXIT : "; 
          cin >> input; 
          switch (input) 
          
          case 1: 
               int input_data; 
               cout << "삽입 할 데이터를 입력하세요 : "; 
               cin >> input_data; 
               stk->push(input_data); 
               break; 
          case 2: 
               stk->pop(); 
               break; 
          case 3: 
               cout << "현재 top 위치의 데이터 : " << stk->peak() << "\n"; 
               break; 
          case 4: 
               stk->print_stack(); 
               break; 
          case 5: 
               cout << "프로그램을 종료합니다." << "\n"; 
               return 0; 
          default : 
               cout << "잘못 된 입력입니다. 다시 입력해 주세요" << "\n"; 
          
     
}


질문 사항은 댓글로 남겨주세요!




* 문제
n개의 정수로 이루어진 임의의 수열이 주어진다. 우리는 이 중 연속된 몇 개의 숫자를 선택해서 구할 수 있는 합 중 가장 큰 합을 구하려고 한다. 단, 숫자는 한 개 이상 선택해야 한다.
예를 들어서 10, -4, 3, 1, 5, 6, -35, 12, 21, -1 이라는 수열이 주어졌다고 하자. 여기서 정답은 12+21인 33이 정답이 된다.

* 풀이
수열의 원소중 연속된 숫자를 선택해 가장 큰 합을 구하기 위해서 동적계획법(Dynamic Programming)을 사용합니다. 첫번째 원소부터 별도의 배열(dp배열)에 수열값과 그전까지 합한 값 중 큰값을 저장합니다. 그중 최댓값을 구하면 정답입니다. 이해가 안가시면 아래 코드를 주석과 함께 한 줄 씩 해석해 보시기 바랍니다.

* 소스코드

#include <iostream> 
#define MAX(a,b) ((a) > (b) ? (a) : (b)) 
using namespace std; 

int main(){ 
     int input_value = 0; 
     cin >> input_value; 
     int *data = new int[input_value]; 
     int *dp = new int[input_value];  // 배열 동적 할당
     int max = -1001; // max는 입력 값 중 가장 작은값보다 작은값

     for (int i = 0; i < input_value; i++)  
          cin >> data[i]; // 데이터 입력

     dp[0] = data[0]; // 첫번째 원소는 그대로 대입(그 자체로 최대값)

     for (int i = 1; i < input_value; i++) 
          dp[i] = MAX(data[i], data[i] + dp[i - 1]); 
          // dp배열에 원소 값과 그전 dp값을 합한 값 중 큰 값 대입

     for (int i = 0; i < input_value; i++) 
          if (max < dp[i]) max = dp[i]; // 최댓값 검색

     cout << max; 

     delete[] dp; 
     delete[] data; // 배열 동적 할당 메모리 해제

     return 0; 
}

질문 및 오류사항은 댓글로 달아주세요.


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

정렬 알고리즘 종류  (0) 2018.09.12
검색 알고리즘 종류  (2) 2018.09.11
자료구조 그래프(Graph)  (1) 2018.09.06
자료구조 힙(Heap)  (0) 2018.09.05
자료구조 이진 탐색 트리(Binary Search Tree)  (0) 2018.09.01
자료구조 트리(Tree)  (0) 2018.08.30
[자료구조] 큐 QUEUE  (0) 2018.08.19
[자료구조] 스택 STACK  (0) 2018.08.19

to Top