julia coding story

B+ Tree 본문

소프트웨어/Algorithm

B+ Tree

julia-biolat 2022. 10. 18. 15:04
728x90

□ B+ tree개념

: 실세계에서 가장 널리 활용되는 B-tree

: 인덱스 구조와 시퀀스셋(데이터셋)으로 구성

: 인덱스 구조의 루트를 제외한 모든 노드들은 at least half full(절반 이상) 조건을 만족함 (단, 초기 상황은 예외)

 

▶ 인덱스 구조

○ 각 노드가 레코드 대신 (오른쪽으로 갈지, 왼쪽으로 갈지만 알려주는) key 값 만으로 구성된 B-tree

○ 노드의 key 값은 데이터 노드로 찾아가는데 이요하는 navigation에 이용

- 노드의 key 값보다 작거나 같으면 search_key <= node_key 바로 좌측 포인터로 이동

- 노드의 key값보다 크면 search_key>node_key 그 노드의 다음 key 값과 비교

- 만약 다음 key 값이 없으면 그 노드의 맨 우측 포인터로 이동

○ B-tree의 requirements(필요조건)와 constraints(제한)을 만족해야한다.

 

▶ 시퀀스 셋(data node set)

○ 모든 레코드를 저장

○ 노드들이 연결리스트를 구성함

○ 모든 레코드들은 key 값 순서로 정렬 (asending sort)

 

□ B+ tree의 장점

1. 인덱스 구조의 노드들은 branching factor를 크게 할 수 있음

• 리프 노드를 제외하고 데이터를 담아주지 않으므로 메모리를 더 확보함으로써 더 많은 key들을 수용할 수 있음

• 하나의 노드에 더 많은 key를 담을 수 있으므로 트리의 높이를 낮출 수 있음

=> 빠른 검색이 가능함 (동일한 메모리에 더 많이 담길 수 있음)

 

2. 풀 스캔시, B+ tree는 리프노드에 데이터가 모두 있으므로 한번의 선형 탐색으로 가능

• B-tree의 경우 모든 노드를 확인해야함

 

3. 범위 검색(Range Search)이 가능

• 리프 노드끼리 연결리스트로 구성되어 있어 순차적인 범위 탐색에 유리

• B+ tree인덱스는 등가 비교와 범위 검색에 사용 가능

• 범위 검색은 먼저 시작하는 key값을 스캔하여 해당 리프 노드를 찾은 다음 리프 노드만을 따라가면서 검색

ex: 만약 key값이 38~55인 레코드들을 찾고 싶다면 38을 찾은 다음 리프 노드에서 정렬되어 있는 링크를 따라 55까지 검색해주면 됨

 

□ Insertion of B+ tree : 첫 레코드의 삽입

1. 첫 레코드의 삽입 : 아직 하나의 레코드도 삽입되지 않은 상황 (Root가 NULL인 경우)

• 인덱스 노드를 하나 만들고 이를 루트가 가르키게 함

• 데이터 노드를 하나 만들고 레코드를 넣음

• 루트 인덱스 노드 Pointer[0] (데이터 노드에 대한 포인터)가 이 데이터 노드를 가리키게 함

• 루트 인덱스 노드의 pointer[1]에 NULL을 넣음 => 초기상황 삽입임을 나타냄

 

if (!ROOT)
	{	// 처음으로 하나의 레코드를 삽입하게 된다.
		ROOT = (type_ptr_idxnode)malloc(sizeof(type_idx_node));
		HEAD = (type_ptr_datanode)malloc(sizeof(type_data_node));

		HEAD->rec[0] = in_rec; // data node 에 첫 레코드를 넣음.
		HEAD->fill_cnt = 1;
		HEAD->link = NULL;

		ROOT->ptri[0] = NULL; // 리프노드임을 나타냄.
		ROOT->ptrd[0] = HEAD;
		ROOT->ptrd[1] = NULL;  //  초기상황임을 나타냄.
		strcpy(ROOT->key[0], in_rec.name);		// 키를 인덱스 노드에  넣음.
		ROOT->fill_cnt = 1;
		return 1;  // 첫 레코드의 넣기 종료.
	}

 

 

2. Root가 NULL이 아닌 경우 초반부 삽입

• Sequence set에 데이터 노드 하나만 존재하는 경우

• Root 인덱스 노드의 pointer[1]이 NULL인지를 체크하여 확인함

• 초반부 삽입이면 무조건 첫 데이터 노드에 삽입하는 것을 시도 (즉, index 구조에서의 navigation과정을 생략)

2.1) 데이터 노드에 빈자리가 있으면 순서에 맞춰서 삽입 후 종료 (최대 레코드가 변경되었다면, 루트 인덱스 노드의 key[0]도 이것으로 변경)

else if (!ROOT->ptri[0] && !ROOT->ptrd[1])
	{   // 초기상황임.
		fc = HEAD->fill_cnt;
		if (fc < MAXD_data) {	// 아직 full 은 아님. 적당한 오름 차순 위치에 넣음.
			for (i = 0; i < fc; i++) {
				if (strcmp(in_key, HEAD->rec[i].name) < 0)
					break;  // i 가 결정됨.
				else if (strcmp(in_key, HEAD->rec[i].name) == 0) {
					//printf("동일키 이미 존재로 삽입 실패! \n"); 
					return 0;
				}
				else; // try next i.
			}
			// i 부터 우측을 한칸씩 우측으로 시프트함.
			for (j = fc; j > i; j--) HEAD->rec[j] = HEAD->rec[j - 1];
			HEAD->rec[i] = in_rec; // 새 레코드를 넣음.
			HEAD->fill_cnt++;
			strcpy(ROOT->key[0], HEAD->rec[fc].name);	// 데이타노드의 마지막 레코드의 키를 부모에게 넣어줌. 여기 중요!!
			return 1; // 성공으로 종료.
      }

 

2.2) 데이터 노드에 빈자리가 없다면 데이터 노드 splitting 진행(overflow처리)

• 삽입할 레코드와 데이터 노드의 모든 레코드를 big node에 넣고 두 부분으로 분리

• big node의 중간 레코드까지 첫 부분으로 하여 현재 데이터 노드에 넣고, 뒷 부분은 새로운 데이터 노드를 햘당하여 넣음

• 중간 레코드의 key 값 = 루트 인덱스 노드의 key[0]

• 새로운 데이터 노드(new_ptrd)에 대한 포인터를 루트 인덱스 노드의 pointer[1]에 넣음

• 두 데이터 노드를 연결 : link of data node 1 = pointer to data node 2 / link of data node 2 = NULL

else
		{   // 첫 데이타노드가 full 임. split 가 필요함.
			// big data node 를 준비함
			for (i = 0; i < MAXD_data; i++)
			{
				if (strcmp(in_key, HEAD->rec[i].name) < 0)
					break;
				else if (strcmp(in_key, HEAD->rec[i].name) == 0) {
					//printf("동일키 이미 존재로 삽입 실패! \n"); 
					return 0;
				}
				else; // try next i.
			}
			for (j = 0; j < i; j++) // i 이전 부븐을 빅노드로 가져옴.
				bnode_data.rec[j] = HEAD->rec[j];
			bnode_data.rec[j] = in_rec;
			j++;
			while (i < MAXD_data)
			{
				bnode_data.rec[j] = HEAD->rec[i];
				j++;		//i와 j를 증가 시킨다. 
				i++;
			}
			// index of center record is at D_data. center 및 그 좌측은 노드 HEAD 로 옮긴다.
			for (i = 0; i <= D_data; i++)
				HEAD->rec[i] = bnode_data.rec[i];
			HEAD->fill_cnt = D_data + 1;
			new_ptrd = (type_ptr_datanode)malloc(sizeof(type_data_node));

			for (i = 0; i < D_data; i++)	// 나머지 뒤는 새로운 노드에 넣음.
				new_ptrd->rec[i] = bnode_data.rec[i + 1 + D_data];
			new_ptrd->fill_cnt = D_data;

			strcpy(ROOT->key[0], HEAD->rec[D_data].name); // 앞 데이타노드의 맨 뒤 레코드의 키를 부모에 넣음
			ROOT->ptrd[1] = new_ptrd;	// 이 작업으로 지금부터 정상적인 B+  트리가 됨.

			HEAD->link = new_ptrd;	// 연결리스트에 새 노드를 연결함.
			new_ptrd->link = NULL;	// 새 데이타 노드가 연결리스트의 마지막 노드임.
			return 1; // 성공으로 종료.
		}
	} // 초기상황 삽입 종료.

3. 일반적인 경우

○ 루트 인덱스 노드의 pointer[1]이 NULL인지 아닌지 체크하여 확인

○ Root에서 출발하여 삽입할 레코드가 들어가야 할 데이터 노드를 탐색

• index 구조를 nevigation

• new key <= key in node 비교에 의해 아래로 내려가는 작업을 반복

• 못 찾으면 그 노드의 맨 우측 포인터를 따라 내려감

• 내려가다가 결국 데이터 노드에 도달

// (ROOT가 NULL 이거나 초기상황임)이 아니면 여기로 온다. 즉 B-tree 인덱스는 정상 B-tree 임.
	// 먼저 탐색을 통하여 in_rec 이 들어갈 데이타노드를 찾는다. 

	curr = ROOT;	//root의 값을 curr에 넣는다. 

	// 인덱스 구조 내비게이션.
	top = -1;	// 스택을 empty 로 초기화한다.
	if (ROOT->ptri[0] != NULL) {	// ROOT 가 리프노드가 아님. 
		do {
			for (i = 0; i < curr->fill_cnt; i++) {   // 자식으로 내려갈 포인터를 찾음.
				if (strcmp(in_key, curr->key[i]) <= 0)
					break; // 자식으로 내려 가기 위함
			}
			push(curr); // 나중에 부모를 찾기 위해 저장함.
			curr = curr->ptri[i];	// 자식으로 내려 감.
			if (curr->ptri[0] == NULL)
				break;	// 내려 간  새 curr 노드가 리프노드임.
		} while (1);
	}	// 인덱스구조 내비게이션.

	// curr 는 인덱스구조의 리프노드임. 여기서는 데이타노드로 내려가야 함.
	for (i = 0; i < curr->fill_cnt; i++)
		if (strcmp(in_key, curr->key[i]) <= 0)
			break;

 

○ 찾은 데이터 노드에 삽입 시도

• 경우1) 빈 자리가 있는 경우 : 순서에 맞춰 삽입하고 종료

parent = curr; // 리프노드 저장. (주의: 스택에 넣지는 않음.)
	curr_d = curr->ptrd[i]; // 데이타노드로 내려 감. curr_d 은 데이타노들 가리킴.
	down_idx = i;	// 부모에서 자식으로 내려간 인덱스 저장. 나중에 이용할 것임.

	// 이 데이타노드(curr_d)에 in_rec 레코드를 넣어야 함.

	fc = curr_d->fill_cnt;
	if (fc < MAXD_data) 	// 아직 full 은 아님. 적당한 오름 차순 위치에 넣음.
	{
		for (i = 0; i < fc; i++) {
			if (strcmp(in_key, curr_d->rec[i].name) < 0) break;  // i 가 결정됨.
			else if (strcmp(in_key, curr_d->rec[i].name) == 0)
			{ //printf("동일키 이미 존재로 삽입 실패!\n"); 
				return 0;
			}
			else; // try next i.
		}
		// i 부터 우측을 한칸씩 우측으로 시프트함.
		for (j = fc; j > i; j--)
			curr_d->rec[j] = curr_d->rec[j - 1];
		curr_d->rec[i] = in_rec; // 새 레코드를 넣음.
		curr_d->fill_cnt++;

		if (down_idx < parent->fill_cnt)	// 마지막 포인터로 내려 온 경우는 부모에 반영하지 않음.
			strcpy(parent->key[down_idx], curr_d->rec[fc].name);// 데이타노드의 마지막 레코드의 키를 부모에게 넣어줌. 여기 중요!!
		return 1; // 성공으로 종료.
	}

• 경우 2) 빈 자리가 없는 경우 : overflow 발생

 

○ 일반적인 경우 : 경우 2의 overflow 발생

• 삽입할 레코드와 도달한 데이터 노드의 모든 레코드를 big node에 넣고 두 부분으로 분리

• Big node의 중간 레코드 (middle record)까지를 첫 부분으로 하여 도달한 데이터 노드에 넣고, 뒷부분은 새로운 데이터 노드를 할당하여 넣음

• 새로운 데이터 노드에 대한 포인터를 ptr_new_data라 하자.

• middle record의 key를 mid_key라 하자

<mid_key, ptr_new_data> 쌍을 도달한 데이터 부모 노드에 삽입 

else
	{ 	// 첫 데이타노드가 full 임. split 가 필요함. big data node 를 준비함
		for (i = 0; i < MAXD_data; i++) {
			if (strcmp(in_key, curr_d->rec[i].name) < 0) break;
			else if (strcmp(in_key, curr_d->rec[i].name) == 0)
			{ //printf("동일키 이미 존재로 삽입 실패!\n"); 
				return 0;
			}
			else; // try next i.
		}
		for (j = 0; j < i; j++) // i 이전 부분을 빅노드로 가져옴.
			bnode_data.rec[j] = curr_d->rec[j];
		bnode_data.rec[j] = in_rec;
		j++;
		while (i < MAXD_data) {
			bnode_data.rec[j] = curr_d->rec[i];
			j++;		//i와 j를 증가 시킨다. 
			i++;
		}
		// center record is at index D_data. center 및 그 좌측은 노드 curr_d 로 옮긴다.
		for (i = 0; i <= D_data; i++) curr_d->rec[i] = bnode_data.rec[i];
		curr_d->fill_cnt = D_data + 1;

		new_ptrd = (type_ptr_datanode)malloc(sizeof(type_data_node));
		for (i = 0; i < D_data; i++)	// 나머지 뒤는 새로운 노드에 넣음.
			new_ptrd->rec[i] = bnode_data.rec[i + 1 + D_data];
		new_ptrd->fill_cnt = D_data;
		new_ptrd->link = NULL;

		// 시퀀스셋 연결리스트에 이 새로운 노드를 curr 다음 노드가 되게 끼워 넣어야 한다.
		new_ptrd->link = curr_d->link;
		curr_d->link = new_ptrd;

		// 쌍 [center키, new_ptrd] 을 인덱스구조의 리프노드인 부모 노드에 넣는 새로운 문제가 생긴다.
		// 아래 3 줄로 변수 설정후에 아래의 b-tree 삽입 루프로 간다.
		curr = parent;
		strcpy(in_key, curr_d->rec[D_data].name);	// 쌍의 원소 1.
		child_d = new_ptrd;					// 쌍의 원소 2.
	} // 데이타노드가 full 인 곳에 삽입.

 

* B-tree에 하나의 쌍을 넣음

 

	// 여기서 부터는 B-tree 에 하나의 쌍을 넣는 작업을 한다.

	do {
		// 여기에 오기 전에 변수 in_key 에 넣을 키,  변수 child_d 또는 child 에 포인터를 설정하고 온다.
		// The pair <in_key, child_d> 또는 <in_key, child>  를 curr 에 넣어야 한다. 
		// child_d: 데이타노드를 포인트,  child: 인덱스노드를 포인트. 둘 중 무엇을 이용할지는 curr의 리프/비리프 여부로 결정.

		if (curr->fill_cnt < MAXD_idx)	// curr 에 빈자리가 있다. 이 curr 노드에 넣는다.
		{
			for (i = 0; i < curr->fill_cnt; i++) //i를 curr->fill_cnt까지 증가하면서 조사한다.
				if (strcmp(in_key, curr->key[i]) < 0) //i 에서의 키가 in_key 보다 더 크면 멈춘다. 
					break;

			for (j = curr->fill_cnt; j > i; j--) { // i 부터 우측 부분을 포인터와 키를  한칸씩 우로 시프트 한다.
				curr->ptrd[j + 1] = curr->ptrd[j];
				curr->ptri[j + 1] = curr->ptri[j];
				strcpy(curr->key[j], curr->key[j - 1]);
			}

			strcpy(curr->key[i], in_key);	// 키 삽입 
			if (curr->ptri[0] == NULL) {	// curr 는 리프노드이므로 type-1 삽입이다.
				curr->ptrd[i + 1] = child_d;	// 자식 포인터를 ptrd 에 삽입 
				curr->ptri[i + 1] = NULL;
			}
			else { // curr 는 리프노드가 아니다. 고로 type-2 삽입이다. 자식 포인터를 ptri 에 넣는다.
				curr->ptri[i + 1] = child;
				curr->ptrd[i + 1] = NULL;
			}

			curr->fill_cnt++;         // fill 카운트 증가시킴 

			return 1; // 삽입 성공으로 종료.
		}
		else 	//   curr 에 빈자리가 없다. 인덱스노드의 빅노드 이용하여 split & elevation 을 수행한다.
		{
			for (i = 0; i < MAXD_idx; i++) {
				if (strcmp(in_key, curr->key[i]) < 0) // 만약 i 의 키가 in_key가 크면 멈춤
					break;
			}

			bnode_index.ptri[0] = curr->ptri[0]; // 먼저 포인터 0 을 옮김.
			bnode_index.ptrd[0] = curr->ptrd[0]; //   "   "

			for (j = 0; j < i; j++) {	// i 전까지 curr 의 내용을 옮김.
				strcpy(bnode_index.key[j], curr->key[j]);
				bnode_index.ptri[j + 1] = curr->ptri[j + 1];
				bnode_index.ptrd[j + 1] = curr->ptrd[j + 1];
			}

			strcpy(bnode_index.key[j], in_key); // 쌍의 첫 원소 in_key 를 넣어 준다.
			// 쌍의 원소2 인  포인터를넣어 준다.
			if (curr->ptri[0] == NULL) { // curr 는 리프노드이므로 type-1 삽입이다.
				bnode_index.ptrd[j + 1] = child_d;   // 자식 포인터를 ptrd 에 삽입 
				bnode_index.ptri[j + 1] = NULL;  // 이것은 넣지 않아도 문제는 없다.
			}
			else { // curr 는 리프노드가 아니다. 고로 type-2 삽입이다. 자식 포인터를 ptri 에 넣는다.
				bnode_index.ptri[j + 1] = child;
				bnode_index.ptrd[j + 1] = NULL;  // 이것은 넣지 않아도 동작은 잘 한다.
			}

			j++; // j 증가시킴 

			while (i < MAXD_idx) {
				strcpy(bnode_index.key[j], curr->key[i]);  //curr의 레코드를 bnode 로 복사
				bnode_index.ptri[j + 1] = curr->ptri[i + 1]; // 그 우측의 포인터도 복사.
				bnode_index.ptrd[j + 1] = curr->ptrd[i + 1];
				j++;		//i와 j를 증가 시킨다. 
				i++;
			}

			// 빅노드를 반으로 나누어 전반부를 curr 에 넣는다.
			for (i = 0; i < D_idx; i++) {
				curr->ptri[i] = bnode_index.ptri[i];    //빅노드의 앞의 반을 현재 노드에 넣어준다. [레코드, 포인터]
				curr->ptrd[i] = bnode_index.ptrd[i];
				strcpy(curr->key[i], bnode_index.key[i]);
			}

			curr->ptri[i] = bnode_index.ptri[i];		//bnode.ptr[i]는 curr.ptr[i]를 가르킨다. 
			curr->ptrd[i] = bnode_index.ptrd[i];
			curr->fill_cnt = D_idx;

			new_ptri = (type_ptr_idxnode)malloc(sizeof(type_idx_node)); // 빅노드의 후반부를 새로운 노드에 넣는다. 

			for (i = 0; i < D_idx; i++) {	//뒤의 반을 새노드에 저장 
				new_ptri->ptri[i] = bnode_index.ptri[i + 1 + D_idx];
				new_ptri->ptrd[i] = bnode_index.ptrd[i + 1 + D_idx];
				strcpy(new_ptri->key[i], bnode_index.key[i + 1 + D_idx]);
			}
			new_ptri->ptri[i] = bnode_index.ptri[MAXD_idx + 1];	// 맨 마지막 포인터를 옮긴다.
			new_ptri->ptrd[i] = bnode_index.ptrd[MAXD_idx + 1];
			new_ptri->fill_cnt = D_idx;

			strcpy(in_key, bnode_index.key[D_idx]);	// 빅노드의 중간 키를 elevation 할 키로 함
			child = new_ptri;					// 새로 할당한 노드의 포인터를 elevation 할 포인터로 함.

			if (top >= 0) {	// curr의 부모가 있음.  
				curr = pop();	//  curr 의 부모를 스택에서 가져 와서 그것으로 curr 를 변경.
				// 새로운 curr 에 쌍 [in_key, child] 을 넣는 작업을 수행할 예정임.
			}
			else {    // curr 가 ROOT 이다. 부모가 없다. 부모를 만들고 그것이 부모가 되어야 한다.
				tptr = (type_ptr_idxnode)malloc(sizeof(type_idx_node));
				strcpy(tptr->key[0], in_key);
				tptr->ptri[0] = curr;
				tptr->ptri[1] = child;
				tptr->ptrd[0] = NULL;
				tptr->fill_cnt = 1;
				ROOT = tptr;	// 루트를 변경.
				return 1; // 삽입 성공으로 종료.
			} // else.
		} // else.
	} while (1);

} //함수 insert_arec_b_plus_tree
구분 B-Tree B+ tree
데이터 저장 모든 노드 데이터 저장 가능 오직 리프노드에만 데이터 정장 가능
트리의 높이 높음 낮음 
(한 노드당 key를 더 많이 담을 수 있음)
풀 스캔시 검색 속도 모든 노드 탐색 리프 노드에서 선형 탐색
키 중복 없음 있음
(리프 노드에 모든 데이터가 있으므로)
연결리스트 없음 리프 노드끼리 연결리스트로 연결 

 

 

 

'소프트웨어 > Algorithm' 카테고리의 다른 글

[알고리즘]B-Tree Deletion  (0) 2022.10.15
[알고리즘] B-Tree Insertion  (2) 2022.10.14
[알고리즘] B-Tree Retrieval  (0) 2022.10.14
[알고리즘] B-Tree  (0) 2022.10.04
HashMap의 설명 및 사용방법  (0) 2022.01.05