{"id":655,"date":"2008-07-06T17:53:44","date_gmt":"2008-07-06T17:53:44","guid":{"rendered":"http:\/\/scientopia.org\/blogs\/goodmath\/2008\/07\/06\/b-trees-balanced-search-trees-for-slow-storage\/"},"modified":"2008-07-06T17:53:44","modified_gmt":"2008-07-06T17:53:44","slug":"b-trees-balanced-search-trees-for-slow-storage","status":"publish","type":"post","link":"http:\/\/www.goodmath.org\/blog\/2008\/07\/06\/b-trees-balanced-search-trees-for-slow-storage\/","title":{"rendered":"B-Trees &#8211; Balanced Search Trees for Slow Storage"},"content":{"rendered":"<p> Another cool, but frequently overlooked, data structure in the tree family is called the B-tree. A B-tree is a search tree, very similar to a BST in concept, but optimized differently.<\/p>\n<p> BSTs provide logarithmic time operations, where the performance<br \/>\nis fundamentally bounded by the number of comparisons. B-trees also<br \/>\nprovide logarithmic performance with a logarithmic number of<br \/>\ncomparisons &#8211; but the performance is worse by a constant factor.  The<br \/>\ndifference is that B-trees are designed around a different tradeoff. The B-tree is designed to minimize the number of tree nodes that need to be examined, even if that comes at the cost of doing significantly more<br \/>\ncomparisons.<\/p>\n<p> Why would you design it that way? It&#8217;s a different performance tradeoff.<br \/>\nThe B-tree is a da<br \/>\nta structure designed not for use in memory, but instead for<br \/>\nuse as a structure in hard storage, like a disk drive. B-trees are the<br \/>\nbasic structure underlying most filesystems and databases: they provide<br \/>\nan efficient way of provide rapidly searchable stored structures. But<br \/>\nretrieving nodes from disk is <em>very<\/em> expensive. In comparison to<br \/>\nretrieving from disk, doing comparisons is very nearly free. So the design<br \/>\ngoal for performance in a B-tree tries to minimize disk access; and when disk access is necessary, it tries to localize it as much as possible &#8211; to minimize the number of retrievals, and even more importantly, to minimize the number of nodes on disk that need to be updated when something is inserted.<\/p>\n<p><!--more--><\/p>\n<p> An order-N B-tree is a search tree with the following properties:<\/p>\n<ol>\n<li> each node contains no more than N children.<\/li>\n<li> Every node (except the root and the leaves) contains at least<br \/>\nN\/2 children.<\/li>\n<li> All leaves are empty nodes, and occur in the same level of the tree.<\/li>\n<li> Every node with N children contains N-1 keys.<\/li>\n<li> Given a node N<sub>1<\/sub>, with a K&#8217;th child node N<sub>K<\/sub>, the keys in<br \/>\nN<sub>K<\/sub> are greater than the K-1th key in N<sub>1<\/sub>, and less that<br \/>\nthe Kth key in N<sub>1<\/sub>.<\/li>\n<\/ol>\n<p> The brilliance of the B-tree is that it&#8217;s a structure that minimizes access to disk while maintaining a nearly perfect balance. It&#8217;s really an amazingly beautiful and elegant structure.<\/p>\n<p> The key to it is the beautiful insert operation. The tree grows from the leaves &#8211; but it only adds levels at the root. That makes the balancing work &#8211; and work in a way that guarantees that you&#8217;ll be able to keep the tree in balance doing nothing but modifying nodes in a single linear chain from the leaf up to the root.<\/p>\n<p> To insert a value, you start by picking the correct place to insert. The set of non-empty nodes at the bottom of the tree is guaranteed to be a sorted list of values,<br \/>\nrunning from the left-most node over to the right, in order.<\/p>\n<p> You find its place in that list &#8211; and that gives you the insertion node. If the<br \/>\ninsertion node already has N values, that you <em>split<\/em> the node. You take the<br \/>\nmedian value in the node, and use it as a pivot. You take the nodes smaller than the<br \/>\npivot, and turn them into a new node named &#8220;L&#8221;; and the nodes larger that the pivot,<br \/>\nand make them into a new node named &#8220;R&#8221;. Then you move the pivot into the parent<br \/>\nof the insert node, and make L its left child, and R its right. So now the former leaf node is now two nodes, with one of its values promoted up into its parent.<\/p>\n<p> If the parent node already had N values, then you split <em>it<\/em>, and so on. You keep bumping up the tree, until you get to the root &#8211; if you need to split the root, then you take the split pivot node, and turn it into a new root with only one value. (That&#8217;s why the second invariant makes an exception for the root node.)<\/p>\n<p> As usual, examples help to understand. Look at the order-3 B-tree in the figure below.<\/p>\n<p><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" alt=\"btree.png\" src=\"https:\/\/i0.wp.com\/scientopia.org\/img-archive\/goodmath\/img_316.png?resize=569%2C173\" width=\"569\" height=\"173\" class=\"inset\" \/><\/p>\n<p> In an order-4 B-tree, every node (except the root) must have at least two values. Suppose we wanted to insert the value &#8220;20&#8221;. That would go in the middle child node, between 18 and 20. But that would result in a new node with 4 values. But the maximum<br \/>\nnumber of values per node is three. So we need to split it. We&#8217;ll pick 18 as the pivot. That will give us two new nodes &#8211; one with only one value (15), and one with two (20, 21). The pivot node, 18, will be inserted into the parent, giving us a parent with the three nodes 10, 18, 30, and four children. The resulting B-tree is shown below.<\/p>\n<p><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" alt=\"btree-one-insert.png\" src=\"https:\/\/i0.wp.com\/scientopia.org\/img-archive\/goodmath\/img_317.png?resize=494%2C225\" width=\"494\" height=\"225\" class=\"inset\" \/><\/p>\n<p> Now, suppose we wanted to insert the value 7. That would split the leftmost child. Using 8 as a pivot, we&#8217;d get two new nodes: (3, 7) and (9), and we&#8217;d insert 8 into the root node. But that would result in the root node having four values. So we split the root node. The root pivot is 18, so we get two new nodes, (8, 10) and (30), and<br \/>\na new root, containing only (18), as shown below.<\/p>\n<p><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" alt=\"btree-second-insert.png\" src=\"https:\/\/i0.wp.com\/scientopia.org\/img-archive\/goodmath\/img_318.png?resize=530%2C331\" width=\"530\" height=\"331\" \/><\/p>\n<p> Deletes are very similar, except that they involve <em>contractions<\/em>. When a node shrinks to be too small because of deletions, it gets merged with a neighbor; after the contraction, if the resulting merged node has too many values (for example, if the neighboring node was full before the merge), then you split the newly merged node. So either you end up removing a node from the tree, or you redistributing the children between the two nodes in a balanced manner.<\/p>\n<p> As usual for my data-structure posts, there&#8217;ll be an example implementation in another post, to follow as soon as I have time to write it.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Another cool, but frequently overlooked, data structure in the tree family is called the B-tree. A B-tree is a search tree, very similar to a BST in concept, but optimized differently. BSTs provide logarithmic time operations, where the performance is fundamentally bounded by the number of comparisons. B-trees also provide logarithmic performance with a logarithmic [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[83],"tags":[],"class_list":["post-655","post","type-post","status-publish","format-standard","hentry","category-data-structures"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/p4lzZS-az","jetpack_sharing_enabled":true,"jetpack_likes_enabled":true,"_links":{"self":[{"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/posts\/655","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/comments?post=655"}],"version-history":[{"count":0,"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/posts\/655\/revisions"}],"wp:attachment":[{"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/media?parent=655"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/categories?post=655"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/tags?post=655"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}