..

B-Tree

B-Tree

트리 형태이지만 2개 이상의 자식 노드를 가질 수 있는 트리

하나의 노드에 여러 개의 키(key) 저장 가능

부모 노드의 key값 범위가 자식 노드를 배치하는데 중요

부모 노드의 key들은 오름차순으로 정렬

B-Tree는 최소 키 한 개, 최소 노드 두 개가 필요하다

 

  • 각 노드는 정렬된 키들을 여러 개 가질 수 있음

  • 하나의 노드는 최대 2t−1개의 키, 최대 2t개의 자식을 가짐
    (t는 최소 차수, 최소 t - 1개 키 보유)

  • 모든 리프는 같은 레벨

  • 노드가 꽉 차면 중간 키를 부모로 올리고 분할(split)

 

루트 노드를 제외한 모든 노드는 최소 ⌈M/2⌉ 개의 자식을 가져야 한다

M개의 자녀를 가질 수 있는 B-Tree를 M차 B-Tree라고 부른다

⌈M - 1⌉ : 각 노드의 최대 key 수

⌈M / 2⌉ : 각 노드의 최소 자녀 노드 수

⌈M / 2 - 1⌉ : 각 노드의 최소 key 수

 

Internal Node의 key수가 x개라면, 자녀의 노드 수는 언제나 x + 1 이다

추가는 항상 leaf 노드에 한다

노드가 넘치면 가운데 key를 기준으로 좌우 key들은 분할하고 가운데 key는 승진한다

 

  • Root Node : 트리의 가장 위에 있는 노드

  • Internal Node(내부 노드) : 자식 노드를 하나 이상 가지고 있는 노드

  • Leaf Node(자녀 노드) : 자식 노드가 없는 끝 노드

 

코드 리뷰

class BTreeNode:
    def __init__(self, t, leaf=False):
        self.t = t  # 최소 차수
        self.leaf = leaf
        self.keys = []       # 현재 노드의 키들
        self.children = []   # 자식 노드들

    def __str__(self, level=0):
        result = "  " * level + str(self.keys) + "\n"
        for child in self.children:
            result += child.__str__(level + 1)
        return result

class BTree:
    def __init__(self, t=2):
        self.t = t
        self.root = BTreeNode(t, leaf=True)

    def search(self, k, node=None):
        if node is None:
            node = self.root
        i = 0
        while i < len(node.keys) and k > node.keys[i]:
            i += 1
        if i < len(node.keys) and k == node.keys[i]:
            return True
        if node.leaf:
            return False
        return self.search(k, node.children[i])

    def insert(self, k):
        root = self.root
        if len(root.keys) == 2 * self.t - 1:
            new_root = BTreeNode(self.t, leaf=False)
            new_root.children.append(self.root)
            self.split_child(new_root, 0)
            self.root = new_root
        self._insert_non_full(self.root, k)

    def _insert_non_full(self, node, k):
        i = len(node.keys) - 1
        if node.leaf:
            node.keys.append(0)  # 공간 확보
            while i >= 0 and k < node.keys[i]:
                node.keys[i + 1] = node.keys[i]
                i -= 1
            node.keys[i + 1] = k
        else:
            while i >= 0 and k < node.keys[i]:
                i -= 1
            i += 1
            if len(node.children[i].keys) == 2 * self.t - 1:
                self.split_child(node, i)
                if k > node.keys[i]:
                    i += 1
            self._insert_non_full(node.children[i], k)

    def split_child(self, parent, i):
        t = self.t
        full_child = parent.children[i]
        new_child = BTreeNode(t, leaf=full_child.leaf)

        # 중간 키를 부모로 올림
        parent.keys.insert(i, full_child.keys[t - 1])
        parent.children.insert(i + 1, new_child)

        # 키/자식 분할
        new_child.keys = full_child.keys[t:]
        full_child.keys = full_child.keys[:t - 1]

        if not full_child.leaf:
            new_child.children = full_child.children[t:]
            full_child.children = full_child.children[:t]

    def print_tree(self):
        print(self.root)

# 테스트
btree = BTree(t=2)
data = [10, 20, 5, 6, 12, 30, 7, 17]

for value in data:
    btree.insert(value)

btree.print_tree()

# 검색 테스트
print("Search 6:", btree.search(6))    # True
print("Search 21:", btree.search(21))  # False

시간 복잡도

M : B-Tree의 차수 (한 노드가 가질 수 있는 최대 자식 수)
N : 트리에 저장된 전체 키의 수

연산 시간 복잡도 설명
검색 (Search) O(logₘ N) 트리의 높이에 비례하며, logₘ N 단계만 내려가면 됨
삽입 (Insert) O(logₘ N) 리프까지 내려간 뒤, 필요 시 분할 발생
삭제 (Delete) O(logₘ N) 리프까지 내려간 뒤, 필요 시 병합/차용 발생