ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • B-tree 자료구조 알아보기
    DataStructure 2023. 10. 30. 21:18
    728x90
    반응형

    - 목차

     

    B-tree 자료구조 알아보기.

    B-tree 자료구조는 Balanced Tree 의 약자인 균형이 잡힌 트리 자료구조입니다.
    여기서 Balance 라는 의미는 B-tree 에게 있어서 가장 중요한 특징인데요.
    빠른 조회 속도와 데이터의 효율적인 저장을 위해서 고안된 방식입니다.
    몇가지 사례를 들어 B-tree 의 Balance 를 설명해보겠습니다.
     

    먼저 B-tree 자료구조는 최소한의 Depth (Height) 를 지향합니다.

    즉, B-tree 자료구조는 일반적인 트리 자료구조에 비해서 높이가 짧습니다.
    이는 조회시에 빠른 속도로 데이터를 조회할 수 있게 합니다.
    이를 구현하기 위해서 하나의 노드가 지닐 수 있는 데이터의 갯수를 최대한 늘립니다.
    하나의 노드가 하나의 데이터를 가지는 것보다 하나 이상의 데이터를 가지게하여 탐색 속도를 늘립니다.
     
    < 일반적인 BST >
    일반적인 BST 에서 8이라는 값을 조회하기 위해서 depth 3 까지 탐색을 이어갑니다.

    Root(5)
    depth: 1 => 1Child(4), 2Child(6)
    depth: 2 => 1-1Child(3), 2-1Child(7)
    depth: 3 => 1-1-1Child(2), 2-1-1Child(8)

     
    < B-tree >
    Degree 가 2인 B-tree 인 경우. (Degree 에 대한 설명은 아래에서 이어가겠습니다.)
    아래와 같이 depth 1 에서 조회가 가능합니다.

    // degree : 2
    Root(2,4,6)
    depth: 1 => 1Child(1), 2Child(3), 3Child(5), 4Child(7,8)

     
     

    B-tree 는 Self-Balancing 기능을 가집니다.

    일반적인 자료구조들은 Skewed 상태가 되기 쉽습니다.
    즉, 한 방향으로 데이터들이 편향되기 쉽죠.
    예를 들어,
     
    < 편향된 BST >

    Root(1)
    Depth: 1 => Child(2)
    Depth: 2 => Child(3)
    Depth: 3 => Child(4)
    Depth: 4 => 4Child(5)
    Depth: 5 => 1Child(6)
    Depth: 6 => 1Child(7)
    Depth: 7 => 1Child(8)
    
    1
      2
        3
          4
            5
              6
                7
                  8

     
     
    < B-tree >

    // degree : 2
    Root(2,4,6)
    depth: 1 => 1Child(1), 2Child(3), 3Child(5), 4Child(7,8)
    
    
       (2,  4,   6)
    (1)  (3)  (5)  (7,8)

     
    위의 사례들처럼 B-tree 는 데이터의 추가/삭제 시에 자체적인 균형을 잡는 기능이 내포되어 있습니다.
    관련된 설명을 이어지는 글에서 상세히 설명하겠습니다.

     

    degree 에 대해서.

    degree 는 B-tree 자료구조가 가지는 제약사항들 중의 하나입니다.
    이러한 제약사항은 B-tree 자료구조가 데이터 조회와 변형 시에 가장 좋은 효율을 낼 수 있도록 합니다.
    degree 는 하나의 노드가 가지는 key 와 자식 노드의 수를 제한합니다.
    ( key 는 하나의 노드가 가지는 데이터들을 의미합니다. )
    만약 B-tree 자료구조의 degree 가 t 인 경우에,
    최소 자식노드의 수는 t,
    최대 자식노드의 수는 2 * t  이 됩니다.
    그리고 하나의 노드가 가지는 데이터의 수는 최소 t - 1 부터 최대 2 * t - 1 까지의 값을 가지게 됩니다.

    예를 들어보겠습니다.

    degree 가 2 인 B-tree 는 최소 1개에서 최대 3개의 key 를 가집니다.
    그리고 최소 2개에서 최대 4개의 자식 노드를 가집니다.
    key 의 갯수 범위: t - 1 ~ 2 * t - 1 = 2 - 1 ~ 2 * 2 - 1 = 1 ~ 3
    자식 노드 갯수 범위: t ~ 2 * t = 2 ~ 2 * 2 = 2 ~ 4
     
    < 1 부터 10 까지 B-tree 삽입 예시 >

    // 1,2,3,4,5 추가
    Root(2)
    child(1) child(3,4,5)
    
    // 6 추가
    Root(2,4)
    child(1) child(3) child(5,6)
    
    // 7 추가
    Root(2,4)
    child(1) child(3) child(5,6,7)
    
    // 8 추가
    Root(2,4,6)
    child(1) child(3) child(5) child(7,8)
    
    // 9, 10 추가
    Root(4)
    1child(2) 2child(6)
    1-1child(1) 1-2child(3) 2-1child(5) 2-2child(7,8,9)
    
    Root(4)
    1child(2) 2child(6,8)
    1-1child(1) 1-2child(3) 2-1child(5) 2-2child(7) 2-3child(9,10)
    
    


    위 예시는 1 부터 10 까지 데이터를 B-tree 에 삽입하는 예시입니다.
    degree 의 제약사항처럼 key 의 갯수와 자식노드의 수는 범위 제약을 어기지 않습니다.

    degree 가 3 인 B-tree 의 예시입니다.
    degree 가 3 인 경우에는 하나의 노드가 2개에서 5개의 key를 가집니다.
    그리고 3개에서 6개의 자식노드를 가질 수 있습니다.

    < 1 부터 12 까지 삽입하는 예시 >

    // 1,2,3,4,5 추가
    Root(1,2,3,4,5)
    
    // 6,7,8 추가
    Root(3)
    child(1,2) child(4,5,6,7,8)
    
    // 9추가
    Root(3,6)
    child(1,2) child(4,5) child(7,8,9)
    
    // 10,11 추가
    Root(3,6)
    child(1,2) child(4,5) child(7,8,9,10,11)
    
    // 12 추가
    Root(3,6,9)
    child(1,2) child(4,5) child(7,8) child(10,11,12)
    

     

    Root Node 와 Leaf Node 의 예외.


    모든 노드가 Degree 에 따른 갯수 제한이 있는 것은 아닙니다.
    오직 Internal Node 가 해당 제약 사항을 적용받는 대상인데요.
    Root Node 와 Leaf Node 는 구조적으로 해당 제약을 덜 받습니다.
    Root Node 와 Leaf Node 의 제약사항에 대해서 알아보도록 하겠습니다.


    Root Node.

    Root Node 는 최소 자식 노드와 최소 key 갯수에 대한 제약을 받지 않습니다.
    왜냐하면 초기 상태의 B-tree 는 데이터가 부족한 상태이기 때문에 Degree 에 따른 최소갯수를 맞출 수 없습니다.

    Degree 가 3인 B-tree 는 최소 2개의 key 와 최소 3개의 자식 노드를 가져야합니다.
    하지만 데이터가 최소 11 개의 데이터가 삽입되기 전까진 제약사항을 지킬 수 없습니다.
    ( 하나의 노드는 5개 이상의 key 를 가지므로 11개의 데이터가 삽입되면 구조적으로 Node Split 이 두번 발생 )

    Leaf Node.

    Leaf Node 는 트리 구조의 제일 하위에 존재하는 노드입니다.
    Leaf Node 는 애초에 자식 노드를 가지지 않기 때문에 자식 노드에 대한 제한이 없습니다.
     
     

    Node Split.


    Node Split 은 B-tree 의 Self-Balancing 의 핵심입니다.
    Node Split 은 key 의 최대 갯수 이상으로 데이터가 추가되었을 때에 발생하며,
    하나의 노드가 두개의 노드로 나뉘어지고,
    각각의 노드가 key 를 나누어가지게 됩니다.

    하나의 노드가 최대 갯수 이상의 key 를 가지게 되는 상태을 overfull 이라고 합니다.
    즉, overfull 상태의 노드가 Node Split 의 대상 노드가 되죠.

    아래 예시들은 각 Degree 에서 overfull 상태를 보여줍니다.

    < Degree 2 인 B-tree 의 overfull >
     

    // 1,3,5 추가
    // Root Node overfull
    Root(1,3,5)
    
    // 7 추가
    // Node Split 발생
    Root(3)
    Child(1) Child(5,7)
    
    // 9 추가
    // child node overfull
    Root(3)
    Child(1) Child(5,7,9)
    
    // 11 추가
    // Node Split 발생
    Root(3,7)
    Child(1) Child(5) Child(9,11)

     
    < Degree 3 인 B-tree 의 overfull >

    // 1,3,5,7,9 추가
    // Root Node overfull
    Root(1,3,5,7,9)
    
    // 11 추가
    // Node Split 발생
    Root(5)
    Child(1,3) Child(7,9,11)
    
    // 13,15 추가
    // Child Node overfull
    Root(5)
    Child(1,3) Child(7,9,11,13,15)
    
    // 17 추가
    // Node Split 발생
    Root(5,11)
    Child(1,3) Child(7,9) Child(13,15,17)


    위와 같은 방식으로 overfull Node 는 Node Split 이 발생하게 됩니다.



    관련 예시.


    < B-tree 파이썬 코드 >

    class BTreeNode:
        def __init__(self, leaf=True):
            self.keys = []
            self.children = []
            self.leaf = leaf
    
    class BTree:
        def __init__(self, t):
            self.root = BTreeNode()
            self.t = t
    
        def insert(self, key):
            root = self.root
            if len(root.keys) == (2 * self.t) - 1:
                new_root = BTreeNode(leaf=False)
                new_root.children.append(root)
                self._split_child(new_root, 0)
                self.root = new_root
                self._insert_non_full(new_root, key)
            else:
                self._insert_non_full(root, key)
    
        def _insert_non_full(self, node, key):
            i = len(node.keys) - 1
            if node.leaf:
                node.keys.append(None)
                while i >= 0 and key < node.keys[i]:
                    node.keys[i + 1] = node.keys[i]
                    i -= 1
                node.keys[i + 1] = key
            else:
                while i >= 0 and key < node.keys[i]:
                    i -= 1
                i += 1
                if len(node.children[i].keys) == (2 * self.t) - 1:
                    self._split_child(node, i)
                    if key > node.keys[i]:
                        i += 1
                self._insert_non_full(node.children[i], key)
    
        def _split_child(self, parent, index):
            t = self.t
            child = parent.children[index]
            new_child = BTreeNode(leaf=child.leaf)
    
            parent.keys.insert(index, child.keys[t - 1])
            parent.children.insert(index + 1, new_child)
    
            new_child.keys = child.keys[t: (2 * t) - 1]
            child.keys = child.keys[0: t - 1]
    
            if not child.leaf:
                new_child.children = child.children[t: 2 * t]
                child.children = child.children[0: t]
    
        def search(self, key, node=None):
            if node is None:
                node = self.root
    
            i = 0
            while i < len(node.keys) and key > node.keys[i]:
                i += 1
    
            if i < len(node.keys) and key == node.keys[i]:
                return True
            elif node.leaf:
                return False
            else:
                return self.search(key, node.children[i])
    
        def display(self, node=None, level=0):
            if node is None:
                node = self.root
            print(f"Level {level}: {node.keys}")
            level += 1
            if not node.leaf:
                for child in node.children:
                    self.display(child, level)
    
    # Usage example:
    if __name__ == "__main__":
        b_tree = BTree(2)
        keys = [3, 8, 1, 4, 2, 6, 7]
        for key in keys:
            b_tree.insert(key)
        
        b_tree.display()
        print("Search for key 4:", b_tree.search(4))
        print("Search for key 5:", b_tree.search(5))
    

     

    1 부터 6 까지 데이터가 추가되는 경우.

    1,2,3,4,5,6
    
    // 1 추가
    Root(1)
    
    // 2 추가
    Root(1,2)
    
    // 3 추가
    Root(1,2,3)
    
    // 4 추가
    Root(2)
    1-1Child(1), 1-2Child(3,4)
    
    // 5 추가
    Root(2)
    1-1Child(1), 1-2Child(3,4,5)
    
    // 6 추가
    Root(2,4)
    1-1Child(1), 1-2Child(3), 1-3Child(5,6)

     

    7 이 추가되는 경우.


    leaf node 의 경우에는 key 의 갯수가 2 * t - 1 인 최댓값에 이르더라도 당장 Node Split 되지는 않습니다.
    이는 leaf node 의 예외 규칙입니다.

    Root(2,4)
    1-1Child(1), 1-2Child(3), 1-3Child(5,6)
    
    // 7 추가
    Root(2,4)
    1-1Child(1), 1-2Child(3), 1-3Child(5,6,7)

     

    8 이 추가되는 경우.

    8이 추가되는 적절한 위치의 노드는 3Child 노드입니다.
    하지만 3Child 노드는 Overfull 상태입니다.
    5,6,7 인 3개의 key 가 존재하기 때문입니다. (2 * t - 1 == 3)
    따라서 3Child 노드는 Node Split 이 작동하며,
    Median Key : 6, Left Subset : [5], Right Subset : [7,8]
    이 됩니다.
    따라서 4Child 노드가 새롭게 생성되고, 7,8 이라는 key 를 가집니다.
    기존의 3Child 노드는 Left Subset 인 5 를 key 로 가지며,
    Median Key 인 6 은 Parent Node 인 Root Node 로 보내집니다.

    // 7이 추가된 상태
    Root(2,4)
    1Child(1), 2Child(3), 3Child(5,6,7)
    
    
    // 8 추가
    Root(2,4)
    1Child(1), 2Child(3), 3Child(5,6,7,8)
    
    // 8 추가된 이후에 Node Split
    Root(2,4,6)
    1Child(1), 2Child(3), 3Child(5), 4Child(7,8)

     

    9 가 추가되는 경우.

    9가 추가될때, Root Node 는 Overfull 상태입니다.
    Root Node 가 Overfull 상태이면, Root Node 의 Split 가 우선적으로 선행됩니다.
    Root(2,4,6) 이 Split 되면, Root(4), 1Child(2), 2Child(6) 와 같이 분리가 됩니다.
    그리고 Root(2,4,6) 가 가지는 Child Node 들은 적절하게 1Child(2), 2Child(6) 하위에 분배됩니다.
     
    1Child(2) -> 1-1Child(1), 1-2Child(3)
    2Child(6) -> 2-1Child(5), 2-1Child(7,8)

    // 8 이 추가된 상태.
    Root(2,4,6)
    1Child(1), 2Child(3), 3Child(5), 4Child(7,8)
    
    // 9 가 추가될 때, 
    // Root 노드가 Overfull 상태라서 Root Node 의 Split 이 선행됨.
    Root(4)
    1Child(2), 2Child(6)
    1-1Child(1), 1-2Child(3), 2-1Child(5), 2-1Child(7,8)
    
    // 9 가 추가.
    Root(4)
    1Child(2), 2Child(6)
    1-1Child(1), 1-2Child(3), 2-1Child(5), 2-1Child(7,8,9)

     
     

     

    반응형
Designed by Tistory.