B-Tree的初衷是希望通过减少存储I / O操作来减少在计算机硬盘驱动器上花费的时间。该技术在数据库和文件系统等计算机领域中发挥了很好的作用。B-Tree及其变体在数据存储方面发挥着前所未有的重要作用。
什么是B-Tree?
B-Tree是一种自平衡树(即所有叶节点具有相同的高度级别)数据结构。但与其他如二叉树,红黑树和AVL树不只有2个子节点不同,B-Tree的节点具有2个以上的子节点。因此,有时它被称为M-way分支树,因为B-Tree中的节点可以具有M个子节点(M> = 2)。
根节点,内部节点和叶节点
内部节点是具有子节点的节点。内部节点位于树的底部上方。在图1中,节点[3 | 6],节点[12 | 15 | 19 | 26]和节点[33 | 36]是内部节点。
叶子节点是没有子节点的节点。它们是树底部的节点。在图1中,节点[1 | 2],节点[4 | 5],节点[20 | 22 | 25]和节点[31 | 32]是一些叶节点。
B-Tree的根节点是一个特殊节点。B-Tree只有一个根节点,它位于树的顶部。根据B-Tree中的项目数,根节点可以是内部节点或叶节点。节点[9 | 30]是图1中B树的根节点。
B-Tree节点的属性
每个节点可以有一堆密钥和一堆子节点(子节点),其中子节点数可以是0或其键的总数加1.让我们考虑节点[x]。如果node [x]是叶子节点,那么它将没有任何子节点。如果它是一个内部节点,那么它的子节点总数是n [x] + 1,其中n [x]是其键的总数。
B-Tree的约束
令t为B树的最小度,其中t > = 2
约束#1:除根节点以外的每个节点必须至少有(t -1)个密钥。它是B-Tree节点中键总数的下限。
约束#2:包含根节点的每个节点必须至多具有(2 t - 1)个密钥。所以我们说如果节点有(2 t - 1)个键,那么节点已满。它是B-Tree节点中键总数的上限。
约束#3:每个内部节点(根节点除外)必须至少有t个子节点。每个内部节点(包括根节点)必须具有至多2 级的儿童。
约束#4:节点中的密钥必须按升序存储。例如,在图1中,节点[12 | 15 | 19 | 按键12 <键15 <键19 <键26
约束#5:密钥左侧的子节点的所有密钥必须小于该密钥。在图1中,位于密钥30左侧的子节点是节点[12 | 15 | 19 | 26],节点[10 | 11],节点[13 | 14],节点[16 | 18],节点[20 | 22 | 25]和节点[28 | 他们的钥匙小于30。
约束#6:密钥右侧的子节点的所有密钥必须大于该密钥。例如,在图1中,位于键9右侧的子节点是节点[12 | 15 | 19 | 26],节点[10 | 11],节点[13 | 14],节点[16 | 18],节点[20 | 22 | 25]和节点[28 | 他们的钥匙大于9。
对于约束#4和约束#5,通常键的左侧的所有节点必须具有小于它的键。
对于约束#4和约束#6,通常键的右侧的所有节点必须具有大于它的键。
在图1中,B树的最小度为3,因此其下界为2,其上界为5。
如果你密切关注图1中B-Tree的例子,你会注意到任何节点中的一个键实际上是该键左侧和右侧较低级别节点中所有键的范围分隔符。
让我们看一下节点[9 | 30]所示,键9左侧的下部节点的键(由蓝色箭头链接的节点中的键)小于9,键9右侧的下部节点的键(节点中的键)由绿色箭头链接的大于9。键30左侧的较低节点的键(由绿色箭头链接的节点中的键)小于30,右侧的较低节点的键(由红色箭头链接的节点中的键)大于30。这种B-Tree模式使得键搜索类似于二叉树的键搜索。
只要B树操作不违反上述约束,它就会自动进行自我平衡。换句话说,约束是这样设计的,以保持其平衡属性。
最小度数(t)与B树高度(h)成反比,增加t将减少h。但是,较大的t意味着在节点中花费更多时间来搜索密钥和遍历子节点。
B-Tree操作的概念和伪代码
左右兄弟节点
节点的左右兄弟是其在同一级别的右侧或左侧的节点。
在图2中,节点的左兄妹[18 | 22]是节点[3 | 12]和它的右兄弟是节点[33 | 36。节点[3 | 2]没有左兄弟,它的右兄弟是节点[18 | 22。节点的左兄妹[33 | 36]是节点[18 | 并且它没有正确的兄弟姐妹。
节点健的前置和后继
在本文中,此处提到的前置节点和后继节点仅适用于内部节点。
前置是左侧子树内的叶节点中数值最大的那个节点。
同样的,后继是右侧子树内的叶节点中数值最小的节点。
在图3中,节点17的前置节点是[13 | 14 | 16] 因为健16其左侧最大的键值。节点17的后继者是节点[19 | 20 |21] 因为键值19是其右侧的最小键值。
拆分节点
对于插入,如果我们要插入的节点已满(有时也称为溢出),我们需要拆分一个节点,使其不违反约束#2。
左右旋转
对于删除,从节点中删除密钥可能违反约束#1(有时也称为下溢)。如果其中一个键的键数大于下限,我们可以将其左侧或右侧兄弟中的一个键移动(借用)到该节点。
与左右兄弟合并
如果无法左/右旋转,则将节点与其兄弟节点合并。
实现B-tree操作的一般概念
更复杂的一些逻辑在于健值的插入,删除动作。B-Tree键插入涉及拆分节点,而键删除涉及旋转和合并。
为了完成键值插入或删除,我们需要遵守的一条重要原则是:在执行操作之前,目标键值必须位于叶子节点中。如果键值位于内部节点中,则需要先将其交换到叶节点中。
键值搜索
<pre style="margin: 0px; padding: 8px 0px 6px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background: rgb(27, 25, 24); border-radius: 0px; overflow-y: auto; color: rgb(80, 97, 109); text-align: start; font-size: 10px; line-height: 12px; font-family: consolas, menlo, courier, monospace, "Microsoft Yahei" !important; border-width: 1px !important; border-style: solid !important; border-color: rgb(226, 226, 226) !important;">
1. `Key-Search (searched-key)`
3. `Current-Processed-Node = Root-Node`
5. `While (Current-Processed-Node is not NULL)`
6. `Current-Index = 0`
8. `While ((Current-Index < key number of Current-Processed-Node) AND`
9. `(searched-key > Current-Processed-Node.Keys[Current-Index]))`
10. `Current-Index++`
11. `End While`
14. `If ((Current-Index < key number of Current-Processed-Node) AND`
15. `(searched-key == Current-Processed-Node.Keys[Current-Index]))`
16. `searched-key is found`
17. `Return it (We are done)`
19. `End If`
21. `Current-Processed-Node = Left/Right Child of Current-Processed-Node`
22. `End While`
24. `Return NULL`
</pre>
键插入
<pre style="margin: 0px; padding: 8px 0px 6px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background: rgb(27, 25, 24); border-radius: 0px; overflow-y: auto; color: rgb(80, 97, 109); text-align: start; font-size: 10px; line-height: 12px; font-family: consolas, menlo, courier, monospace, "Microsoft Yahei" !important; border-width: 1px !important; border-style: solid !important; border-color: rgb(226, 226, 226) !important;">
1. `Split-Node(parent-node, splitted-node)`
3. `Create new-node`
4. `Leaf[new-node] = Leaf[splitted-node] (The new node must have the same leaf info)`
6. `Copy right half of the keys from splitted-node to the new node`
8. `If (Leaf[splitted-node] is FALSE) Then`
9. `Copy right half of the child pointers from spitted-node to the new node`
10. `End If`
12. `Move some of parent children to the right accordingly`
14. `parent-node.children[relevant index] = new-node`
16. `Move some of parent keys to the right accordingly as well`
18. `Parent-node.keys[relevant index] = splitted-node.keys[the right-most index]`
</pre>
<pre style="margin: 0px; padding: 8px 0px 6px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background: rgb(27, 25, 24); border-radius: 0px; overflow-y: auto; color: rgb(80, 97, 109); text-align: start; font-size: 10px; line-height: 12px; font-family: consolas, menlo, courier, monospace, "Microsoft Yahei" !important; border-width: 1px !important; border-style: solid !important; border-color: rgb(226, 226, 226) !important;">
1. `Insert-Key-To-Node(current-node, inserted-key)`
3. `If (Leaf[current-node] == TRUE) Then`
4. `Put inserted-key in the node in ascending order`
5. `Return (We are done)`
6. `End If`
8. `Find the child-node where inserted-key belong`
10. `If (total number of keys in child-node == UPPER BOUND) Then`
11. `Split-Node(current-node, child-node)`
12. `Return Insert-Key-To-Node(current-node, inserted-key)`
13. `End If`
15. `Insert-Key-To-Node(child-node, inserted-key)`
</pre>
<pre style="margin: 0px; padding: 8px 0px 6px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background: rgb(27, 25, 24); border-radius: 0px; overflow-y: auto; color: rgb(80, 97, 109); text-align: start; font-size: 10px; line-height: 12px; font-family: consolas, menlo, courier, monospace, "Microsoft Yahei" !important; border-width: 1px !important; border-style: solid !important; border-color: rgb(226, 226, 226) !important;">
1. `Insert-Key(inserted-key)`
3. `If (root-node is NULL) Then`
4. `Allocate for root-node`
5. `Leaf[root-node] = TRUE`
6. `End If`
9. `If (total number of keys in root-node == UPPER BOUND) Then`
10. `Create a new-node`
11. `Assign root-node to be the child pointer of the new-node`
12. `Assign new-node to be the root-node`
13. `Split-Node(new-node, new-node.children[0])`
14. `End If`
16. `Insert-Key-To-Node(new-node, inserted-key)`
</pre>
其中,Split-Node()和Insert-Key-To-Node()是辅助函数,最终由Insert-Key()调用。根节点的更新由Insert-Key()处理,其余部分由Split-Node()和Insert-Key-To-Node()处理。可以观察到,健值的插入总是发生在叶子节点处。在插入路径期间,如果节点已满,我们需要执行拆分并在该点重新启动插入过程。通过这样做,我们确保B-Tree不会因键值插入产生溢出问题。
键值删除
<pre style="margin: 0px; padding: 8px 0px 6px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background: rgb(27, 25, 24); border-radius: 0px; overflow-y: auto; color: rgb(80, 97, 109); text-align: start; font-size: 10px; line-height: 12px; font-family: consolas, menlo, courier, monospace, "Microsoft Yahei" !important; border-width: 1px !important; border-style: solid !important; border-color: rgb(226, 226, 226) !important;">
1. `Delete-Key-From-Node(parent-node, current-node, deleted-key)`
3. `If (Leaf[current-node] == TRUE) Then`
4. `Search for deleted-key in current-node`
6. `If (deleted-key not found) Then`
7. `Return (We are done)`
8. `End If`
10. `If (total number of keys in current-node > LOWER BOUND) Then`
11. `Remove the key in current-node`
12. `Return (We are done)`
13. `End If`
15. `Get left-sibling-node and right-sibling-node of current-node`
17. `If (left-sibling-node is found AND total number of keys in left-sibling-node > LOWER BOUND) Then`
18. `Remove deleted-key from current-node`
19. `Perform right rotation`
20. `Return (We are done)`
21. `End If`
23. `If (right-sibling-node is found AND total number of keys in right-sibling-node > LOWER BOUND) Then`
24. `Remove deleted-key from current-node`
25. `Perform left rotation`
26. `Return (We are done)`
27. `End If`
29. `If (left-sibling-node is not NULL) Then`
30. `Merge current-node with left-sibling-node`
31. `Else`
32. `Merge current-node with right-sibling-node`
33. `End If`
35. `Return Rebalance-BTree-Upward(current-node)`
36. `End If`
38. `Find predecessor-node of current-node`
40. `Swap the right-most key of predecessor-node and deleted-key of current-node`
42. `Delete-Key-From-Node(predecessor-parent-node, predecessor-node, deleted-key)`
</pre>
<pre style="margin: 0px; padding: 8px 0px 6px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background: rgb(27, 25, 24); border-radius: 0px; overflow-y: auto; color: rgb(80, 97, 109); text-align: start; font-size: 10px; line-height: 12px; font-family: consolas, menlo, courier, monospace, "Microsoft Yahei" !important; border-width: 1px !important; border-style: solid !important; border-color: rgb(226, 226, 226) !important;">
1. `Rebalance-BTree-Upward(current-node)`
3. `Create Stack`
5. `For each step of the path from root-node to current-node Then`
6. `Stack.push(step-node)`
7. `End For`
9. `While (Stack is not empty) Then`
10. `step-node = Stack.pop()`
11. `If (total number of keys in step-node < LOWER BOUND) Then`
12. `Rebalance-BTree-At-Node(step-node)`
13. `Else`
14. `Return (We are done)`
15. `End If`
16. `End While`
</pre>
<pre style="margin: 0px; padding: 8px 0px 6px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background: rgb(27, 25, 24); border-radius: 0px; overflow-y: auto; color: rgb(80, 97, 109); text-align: start; font-size: 10px; line-height: 12px; font-family: consolas, menlo, courier, monospace, "Microsoft Yahei" !important; border-width: 1px !important; border-style: solid !important; border-color: rgb(226, 226, 226) !important;">
1. `Rebalance-BTree-At-Node(step-node)`
3. `If (step-node is NULL OR step-node is root-node) Then`
4. `Return (We are done)`
5. `End If`
7. `Get left-sibling-node and right-sibling-node of step-node`
9. `If (left-sibling-node is found AND total number of keys in left-sibling-node > LOWER BOUND) Then`
10. `Remove deleted-key from step-node`
11. `Perform right rotation`
12. `Return (We are done)`
13. `End If`
15. `If (right-sibling-node is found AND total number of keys in right-sibling-node > LOWER BOUND) Then`
16. `Remove deleted-key from step-node`
17. `Perform left rotation`
18. `Return (We are done)`
19. `End If`
21. `If (left-sibling-node is not NULL) Then`
22. `Merge step-node with left-sibling-node`
23. `Else`
24. `Merge step-node with right-sibling-node`
25. `End If`
</pre>
<pre style="margin: 0px; padding: 8px 0px 6px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background: rgb(27, 25, 24); border-radius: 0px; overflow-y: auto; color: rgb(80, 97, 109); text-align: start; font-size: 10px; line-height: 12px; font-family: consolas, menlo, courier, monospace, "Microsoft Yahei" !important; border-width: 1px !important; border-style: solid !important; border-color: rgb(226, 226, 226) !important;">
1. `Delete-Key(deleted-key)`
3. `Delete-Key-From-Node(NULL, root-node, deleted-key)`
</pre>
根据[2],B-Tree中有两种流行的键值删除方式:
-
直接在tree的最低部执行操作:在进入节点之前先重构树,这样当我们实际从B-Tree中删除一个键时,它的结构不会违反任何约束。删除的伪代码可以在[3]和[4]中找到。
-
查找并删除键值。但是我们需要向上重新平衡,因为键值删除会导致B-Tree的约束违规。我们的健值删除实例基于此策略。删除键值密钥的主方法是Delete-Key()。稍后在Rebalance-BTree-Upward()被调用,以在必要时重组树。重新平衡B树从根本上涉及左/右旋转和左/右兄弟合并。
显示B-Tree的工具