B트리

2019. 4. 29. 18:21알고리즘

개요

º Balanced Tree란?

 

 - 이진 검색 트리의 문제점은 좌우 균형이 맞지 않으면 비효율적. 트리의 성능은 곧 트리의 depth인데, 좌 또는 우로 치우친 경우 O(logN) → O(n^2)까지 성능이 느려질 수 있다.

 - Balanced Tree는 삽입, 삭제 시 필요하면 스스로 균형을 유지한다.

 - ex) AVL Tree, 2-3(-4) Tree, Red-Black Tree, B-Tree 등

 - 항상 (최악의 경우에도) O(logN) 성능을 보장

 

º B-Tree란?

 

 - 하나의 노드에 여러 자료가 배치되는 트리 구조

 - 한 노드에 최대 m개의 자료가 배치되면 → 'M차 B-Tree'

   (단, 모든 노드에 항상 m개의 자료가 배치되어야 하는 것은 아님)

 - m이 짝수냐 홀수냐에 따라 알고리즘이 다르다.

 - 2-3 Tree는 2차 B-Tree와 같고, 2-3-4 Tree는 3차 B-Tree와 같다.

 

#2. 규칙

1. 어떤 노드의 자료 수가 N개라면, 그 노드의 자식의 수는 N+1이어야 한다. 

2. 각 노드의 자료는 정렬된 상태여야 한다.

3. 노드의 자료 Dk의 왼쪽 서브트리는 Dk보다 작은 값들이고, Dk의 오른쪽 서브트리는 Dk보다 큰 값들이어야 한다.

 

(설명)

 

- A B 노드는 C보다 작은 값이다.

- D E 노드는 C보다 크고 F보다 작은 값이다.

- G H 노드는 F보다 큰 값이다.

 

4. Root 노드는 자식이 있다면, 적어도 2개 이상의 자식을 가져야 한다.

5. Root 노드를 제외한 모든 노드는 적어도 m/2개의 자료를 가져야 한다.

  (5차 B-Tree라면 적어도 2개 이상)

6. 외부 노드(leaf)로 가는 경로의 길이는 모두 같다.

  (= 외부 노드는 모두 같은 레벨에 있다.

7. 자료는 중복될 수 없다.

 

 

#3. B-Tree 삽입 알고리즘

- 자료는 항상 Leaf 노드에 추가된다.

- Leaf 노드 선택은 삽입될 키의 하향 탐색에 의하여 결정된다.

- 추가될 Leaf 노드에 여유가 있다면, 그냥 삽입

- 여유가 없다면 노드를 분할한다.

- 분할을 위해서는 키 하나를 부모로 올려야 한다.

- 부모에 여유가 없다면? : 삽입을 위한 하향 탐색을 하면서 꽉 찬 노드는 미리미리 분할을 해줘야 한다.

 

BTree_Insert(value) {

 

while (currentNode is not leaf) {

if (currentNode is full) split(currentNode);

currentNode = proceedToChildNode;

}

 

if (currentNode is full) split(currentNode);

currentNode -> addNodeValue(value);

}

 

1) Root 노드의 분할

 

2) 일반 노드의 분할

 

#4. B-Tree 삭제 알고리즘

- 삭제하려는 자료가 있는 노드는 삭제 후에도 m/2 이상의 자료수를 유지해야 한다.

 

Case #1. 외부 노드 삭제

1) 빌리기 : 형제가 m/2보다 많은 자료를 가지고 있는 경우

 

 

2) 결합하기 : 형제에서 빌릴 수 없는 경우

 

Case #2. 내부 노드 삭제

- 삭제하려는 자료가 존재하는 노드가 내부 노드인 경우, 대체키를 찾아 대체해야 한다. (왼쪽 서브트리의 가장 큰 값 or 오른쪽 서브트리의 가장 작은 값)

- 삭제를 위한 하향 탐색을 하면서 자료수가 m/2 이하인 노드는 빌리기/결합하기

 

 

#5. B-Tree 성능

- M차 B-Tree의 높이는 log_m/2 N 이하이다.

- 검색 시 각 노드당 최대 M번의 순차 검색

- 삽입 / 삭제 / 검색 성능 : O(logN)

 

º B-Tree는 외부 검색에 적합함

  - 하나의 노드 크기를 Disk I/O 단위의 크기로

  - 순차 검색과 트리 검색의 이점을 취함

 

 

코드

 

/*
  프로그램 설명 : C++로 구현한 B-Tree입니다.
*/
#include <iostream> 
#define M 5
using namespace std;

// BTree의 노드 구조체를 선언합니다. 
struct BTreeNode // Public과 동일한 구조체 선언입니다. 
{
 int *data; // 노드에 들어갈 자료의 배열입니다. 
 BTreeNode **childPtr; // 노드 포인트의 배열입니다. 
 bool leaf; // 리프 노드인지 확인합니다. 
 int n; // 자료의 개수를 의미합니다. 
}*root = NULL, *np = NULL, *x = NULL;

// 노드를 초기화하는 함수입니다. 
BTreeNode * init()
{
 int i;
 np = new BTreeNode; // 객체를 할당합니다.
 np->data = new int[M]; // M개까지의 데이터를 가질 수 있습니다.
 np->childPtr = new BTreeNode *[6]; // M + 1개의 자식 노드입니다. 
 np->leaf = true; // 기본적으로 리프 노드입니다. 
 np->n = 0; // 자료의 개수는 0개입니다.
 for(i = 0; i < 6; i++)
 {
  np->childPtr[i] = NULL; // 각각의 자식 노드를 초기화해줍니다. 
 } 
 return np;
}

// 현재 트리를 보여주는 순회 함수입니다. 
void traverse(BTreeNode * p)
{
 cout << endl;
 int i;
 // 즉, 첫번째 자식 노드부터 가장 마지막 자식 노드까지 전부 밑으로 내려가는 것입니다. 
 for(i = 0; i < p->n; i++)
 {
  // 리프 노드가 아니라면 더 밑으로 내려갑니다. 
  if(p->leaf == false)
  {
   traverse(p->childPtr[i]);
  }
  // 데이터를 출력합니다. 
  cout << " " << p->data[i];
 }
 // 리프 노드가 아니라면 더 밑으로 내려갑니다. 
 if(p->leaf == false)
 {
  traverse(p->childPtr[i]);
 }
 cout << endl;
}

// n개 존재하는 데이터 배열 데이터를 정렬하는 함수입니다. 
void sort(int *p, int n)
{
 int i, j, temp;
 for(i = 0; i < n; i++)
 {
  for(j = i; j <= n; j++)
  {
   if(p[i] > p[j])
   {
    temp = p[i];
    p[i] = p[j];
    p[j] = temp;
   }
  }
 }
}

// 자식을 분할하는 함수입니다.
int splitChild(BTreeNode *x, int i)
{

 // x노드를 분할하여 np1과 np3을 할당합니다. 
 int j, mid;
 BTreeNode *np1, *np3, *y;

 /*
  분할된 형태는 다음과 같습니다.
  x -> 왼쪽으로 가고
  np3 -> 그 오른쪽으로 가고
  np1 -> x와 np3의 부모 노드가 됩니다. 
 */ 

 np3 = init();
 np3->leaf = true;

 // x의 부모 노드가 없어 새롭게 부모 노드를 만들어주는 경우입니다. 
 if(i == -1)
 {

  // M의 중간값을 기준으로 분할합니다. 
  mid = x->data[M / 2];
  x->data[M / 2] = 0;
  x->n--;
  np1 = init();

  // np1는 부모노드이므로 리프 노드가 아닙니다. 
  np1->leaf = false;
  x->leaf = true;
  for(j = M / 2 + 1; j < M; j++)
  {

   // np3는 x의 오른쪽 부분 노드를 가져갑니다. 
   np3->data[j - (M / 2 + 1)] = x->data[j];
   np3->childPtr[j - (M / 2 + 1)] = x->childPtr[j];
   np3->n++;

   // x는 반 쪼개서 왼쪽 부분 노드만 가지고 갑니다. 
   x->data[j] = 0;
   x->n--;
  }

  // x의 모든 자식 노드를 NULL로 만듭니다. 
  for(j = 0; j < M + 1; j++)
  {
   x->childPtr[j] = NULL;
  }
  np1->data[0] = mid;
  np1->childPtr[np1->n] = x;
  np1->childPtr[np1->n + 1] = np3;
  np1->n++;

  // 루트 노드로 설정해줍니다. 
  root = np1;
 }

 // 이미 부모 노드가 있는 경우입니다. 
 else
 {
  y = x->childPtr[i];
  mid = y->data[M / 2];
  y->data[M / 2] = 0;
  y->n--;
  for(j = M / 2 + 1; j < M; j++)
  {

   // np3은 x의 오른쪽 자식 부분만 가져갑니다. 
   np3->data[j - (M / 2 + 1)] = y->data[j];
   np3->n++;
   y->data[j] = 0;
   y->n--;
  }
  x->childPtr[i + 1] = y;
  x->childPtr[i + 1] = np3; 
 }
 return mid;
} 

// a라는 원소를 삽입하는 함수입니다. 
void insert(int a)
{
 int i, temp;
 x = root;

 // 만약에 루트 노드가 NULL이라면 
 if(x == NULL)
 {
  root = init();
  x = root;
 }

 // 루트 노드가 NULL이 아니라면 
 else
 {

  // 현재 리프노드이고 크기가 꽉찼다면 
  if(x->leaf == true && x->n == M)
  {
   temp = splitChild(x, -1);
   x = root;

   // 값을 이동하며 삽입될 자리를 찾습니다. 
   for(i = 0; i < (x->n); i++)
   {
    if((a > x->data[i]) && (a < x->data[i + 1]))
    {
     i++;
     break;
    }
    else if(a < x->data[0])
    {
     break;
    }
    else
    {
     continue;
    }
   }
   x = x->childPtr[i];
  }

  // 리프노드가 아닐 때입니다. 
  else
  {

   // 리프 노드까지 이동합니다. 
   while(x->leaf == false)
   {
    for(i = 0; i < (x->n); i++)
    {
     if((a > x->data[i]) && (a < x->data[i + 1]))
     {
      i++;
      break;
     }
     else if(a < x->data[0])
     {
      break;
     }
     else
     {
      continue;
     }
    }

    // 리프 노드 위에서도 만약에 최대치를 넘으면 분할해줍니다. 
    if((x->childPtr[i])->n == M)
    {
     temp = splitChild(x, i);
     x->data[x->n] = temp;
     x->n++;
     continue;
    }
    else
    {
     x = x->childPtr[i];
    }
   }
  }
 }
 x->data[x->n] = a;
 sort(x->data, x->n);
 x->n++;
}

int main(void)
{
 int i, n, t;
 cout << "삽입할 원소의 개수를 입력하세요 : ";
 cin >> n;
 for(i = 0; i < n; i++)
 {
  cout << "원소를 입력하세요 : ";
  cin >> t;
  insert(t);
 } 
 cout << "트리를 순회합니다." << endl;
 traverse(root);
 return 0;
}

 

 

결과값

 

 

'알고리즘' 카테고리의 다른 글

해시 (체이닝, 개방주소)  (0) 2019.04.29
kd-트리  (0) 2019.04.29
레드블랙트리 (자가균형 이진탐색트리)  (0) 2019.04.15
이진 트리 탐색  (0) 2019.04.07