{"id":633,"date":"2008-04-28T11:37:19","date_gmt":"2008-04-28T11:37:19","guid":{"rendered":"http:\/\/scientopia.org\/blogs\/goodmath\/2008\/04\/28\/binary-heaps\/"},"modified":"2008-04-28T11:37:19","modified_gmt":"2008-04-28T11:37:19","slug":"binary-heaps","status":"publish","type":"post","link":"http:\/\/www.goodmath.org\/blog\/2008\/04\/28\/binary-heaps\/","title":{"rendered":"Binary Heaps"},"content":{"rendered":"<p> One of the most neglected data structures in the CS repertoire is<br \/>\nthe heap. Unfortunately, the jargon is overloaded, so &#8220;heap&#8221; can mean<br \/>\ntwo different things &#8211; but there is a connection, which I&#8217;ll explain<br \/>\nin a little while.<\/p>\n<p> In many programming languages, when you can dynamically create<br \/>\nobjects in memory, the space from which you allocate the memory to<br \/>\ncreate them is called the heap. That is <em>not<\/em> what I&#8217;m talking about.<\/p>\n<p> What I am talking about is an extremely simple but powerful structure<br \/>\nthat is used for maintaining a collection of values from which you want<br \/>\nto be able to quickly remove the largest object quickly. This may sound<br \/>\ntrivial, but this turns out to be an incredibly useful structure.  You can use this basic structure to implement a very fast sorting algorithm; to maintain a prioritized set of values\/tasks; to manage chunks of memory; etc. (The<br \/>\nprioritized set is the most common case in my experience, but that&#8217;s probably because I&#8217;ve spent so much of my career working on distributed systems.)<\/p>\n<p><!--more--><\/p>\n<p> When you&#8217;re talking about data structures, one of the important things<br \/>\nyou need to do in order to really understand how to choose the best one is to understand exactly what properties a data structure provides: what operations need to be fast; what operations you need to have, but don&#8217;t care about the speed; how fast things need to be; how much memory you need to create<br \/>\none, and so on.<\/p>\n<p> In a heap, what we want is to be able to add an element<br \/>\nto the heap quickly, and to be able to remove the largest element quickly. We&#8217;re <em>not<\/em> going to remove arbitrary elements of the heap: we only remove the largest. We can&#8217;t query the heap &#8211; that is, we can&#8217;t ask &#8220;Is X in the heap?&#8221; &#8211; all we can do is remove the largest value. And while providing these properties, we want to use as little memory as possible.<\/p>\n<p> Now, I haven&#8217;t yet defined what I mean by &#8220;quickly&#8221; in the above definition. There&#8217;s a good reason for that: there are a set of tradeoffs<br \/>\nthat produce different solutions. For now, we&#8217;ll settle for one<br \/>\nversion of &#8220;quickly&#8221;: O(lg n) &#8211; that is, in the worst case, the amount of time it takes to either remove the largest value, or to add a new value, is<br \/>\nproportional to the logarithm of the number of values in the heap. We can<br \/>\nprovide that kind of performance with a kind of heap called a <em>binary heap<\/em>.<\/p>\n<p> A binary heap is basically a binary tree with three properties, called<br \/>\nthe <em>heap properties<\/em> of the tree:<\/p>\n<ol>\n<li> For any node in the tree, the node is larger than<br \/>\nits children.<\/li>\n<li> Every level of the tree except for the leaves is full.<\/li>\n<li> The leaf level of the tree is full from the leftmost leaf<br \/>\nto the last leaf. (In other words, the last level is filled<br \/>\nleft to right.)\n<\/li>\n<\/ol>\n<p><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" alt=\"heap-example.png\" src=\"https:\/\/i0.wp.com\/scientopia.org\/img-archive\/goodmath\/img_305.png?resize=289%2C218\" width=\"289\" height=\"218\" class=\"inset right\" \/><\/p>\n<p> So, for example, this figure is a binary heap. It&#8217;s got three levels. The first two levels are full; the third level is full on its left side. The next<br \/>\ninsert point would be the next leftmost slot, the left child of 6.<\/p>\n<p> We can write a heap in a textual form using parens: we write each node<br \/>\nas a triple: (x, y, z), where x is the value of the parent, and y and z are the parenthesized heaps that are the children of x. So the same heap that I showed above can be written in text form as:<br \/>\n(11,(10,(9,(8,,),(2,,)),(5,(3,,),(1,,))),(7,(6,,),(4,,))).<\/p>\n<p> To make it easier to read, we can stretch it across a couple of lines:<\/p>\n<pre>\n(11,\n(10,\n(9,\n(8,,),\n(2,,)),\n(5,\n(3,,),\n(1,,))),\n(7,\n(6,,),\n(4,,)))\n<\/pre>\n<p> For the moment, we&#8217;ll ignore just how this tree is represented. We&#8217;ll get<br \/>\nto that later.<\/p>\n<p> It should be obvious how to find the largest element &#8211; it&#8217;s the root of<br \/>\nthe binary heap. But once we remove it, then we don&#8217;t have a heap: there&#8217;s a hole at the top &#8211; so part of the operation of removing the maximum element<br \/>\nhas to be re-validating the heap properties.<\/p>\n<p> The way that we do this is called <em>bubble-down<\/em> What we do is take the last element in the heap &#8211; the rightmost leaf &#8211; and insert it as the root. Now there&#8217;s no hole, but the heap isn&#8217;t valid. So we look at the two children of the root, and take the <em>larger<\/em> one, and swap it with the root. Now the root is larger that both of its children, but the child-heap that now has a new root may be invalid. So we check it, and if it&#8217;s not valid, we repeat<br \/>\nthe bubble-down operation on that child-heap.<\/p>\n<p> Let&#8217;s look at that with an example. Take the heap from the<br \/>\nfigure above. If we remove the largest element (11), and then<br \/>\nreplace it with the last element of the heap, we get the following<br \/>\ninvalid heap:<\/p>\n<pre>\n(1,\n(10,\n(9,\n(8,,),\n(2,,)),\n(5,\n(3,,),))\n(7,\n(6,,),\n(4,,)))\n<\/pre>\n<p> Now, we look at the two children, 10 and 7. 10 is larger, so we swap<br \/>\nthe 1 and the 10, which gives us:<\/p>\n<pre>\n(10,\n(1,\n(9,\n(8,,),\n(2,,)),\n(5,\n(3,,),))\n(7,\n(6,,),\n(4,,)))\n<\/pre>\n<p> Now, we look at the modified subheap &#8211; the left child heap. Its two<br \/>\nchildren are 9 and 5, both larger that 1. 9 is larger, so we swap:<\/p>\n<pre>\n(10,\n(9,\n(1,\n(8,,),\n(2,,)),\n(5,\n(3,,),))\n(7,\n(6,,),\n(4,,)))\n<\/pre>\n<p> Now we&#8217;ve modified the left-left child subheap, and it&#8217;s invalid, so we<br \/>\nagain swap for the larger child:<\/p>\n<pre>\n(10,\n(9,\n(8,\n(1,,),\n(2,,)),\n(5,\n(3,,),))\n(7,\n(6,,),\n(4,,)))\n<\/pre>\n<p> Now our heap is valid again.<\/p>\n<p> So how long did that take? We removed the root, and replaced it with<br \/>\nthe last child. That&#8217;s a couple of steps &#8211; not the dominant cost of the operation. Then\twe did the bubbledown &#8211; that&#8217;s the main cost. So how many<br \/>\nsteps are there in a bubbledown?  In the worst case &#8211; like the example case above &#8211; you&#8217;ll need to do a number of swaps equal to the depth of the tree. The depth of the tree is lg(n), where n is the number of values in the tree. So we do at most lg(n) swaps &#8211; so the cost of removing the largest element<br \/>\nis O(lg n). <\/p>\n<p> What about adding an element? It&#8217;s basically the inverse of removing the root; it&#8217;s a <em>bubble-up<\/em>. We insert the value into the position<br \/>\n<em>after<\/em> the last leaf. Then we compare the leaf to its parent. If it&#8217;s<br \/>\nlarger than its parent, we swap the node with its parent. And we repeat going<br \/>\nup the tree. <\/p>\n<p> Let&#8217;s look at a quick example of insert. Say we wanted to insert 12<br \/>\nto our heap. We&#8217;d start by adding 12 as a leaf:<\/p>\n<pre>\n(10,\n(9,\n(8,\n(1,,),\n(2,,)),\n(5,\n(3,,),\n(<b>12<\/b>,,))\n(7,\n(6,,),\n(4,,)))\n<\/pre>\n<p> It&#8217;s larger than its parent, 5, so we swap:<\/p>\n<pre>\n(10,\n(9,\n(8,\n(1,,),\n(2,,)),\n(<b>12<\/b>,\n(3,,),\n(5,,))\n(7,\n(6,,),\n(4,,)))\n<\/pre>\n<p> And so on &#8211; we&#8217;ll swap two more times, giving us a new heap:<\/p>\n<pre>\n(12,\n(10,\n(8,\n(1,,),\n(2,,)),\n(9,\n(3,,),\n(5,,))\n(7,\n(6,,),\n(4,,)))\n<\/pre>\n<p> So again, we&#8217;re looking at a maximum of lg(n) swaps to<br \/>\ndo an insert. So we&#8217;re O(lg n) for insert.<\/p>\n<p> I&#8217;m running out of writing time, so I&#8217;ll have to leave a<br \/>\ndescription of how to represent a heap, and an example<br \/>\nimplementation of a heap for this evening. But I&#8217;ll leave you with one<br \/>\nquick example of how a heap can be used. Suppose I want to sort<br \/>\na list of integers. We know that the best worst-case time for<br \/>\nsorting is O(n lg n). We can implement a very simple sort by inserting all<br \/>\nof the values into a heap, and then removing them in order. That&#8217;s n inserts, each with maximum cost lg(n); and n removes, maximum cost lg(n). So it&#8217;s<br \/>\nO(n lg n) to sort the list &#8211; and no O(n<sup>2<\/sup>) case like quicksort!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>One of the most neglected data structures in the CS repertoire is the heap. Unfortunately, the jargon is overloaded, so &#8220;heap&#8221; can mean two different things &#8211; but there is a connection, which I&#8217;ll explain in a little while. In many programming languages, when you can dynamically create objects in memory, the space from which [&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-633","post","type-post","status-publish","format-standard","hentry","category-data-structures"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/p4lzZS-ad","jetpack_sharing_enabled":true,"jetpack_likes_enabled":true,"_links":{"self":[{"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/posts\/633","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=633"}],"version-history":[{"count":0,"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/posts\/633\/revisions"}],"wp:attachment":[{"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/media?parent=633"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/categories?post=633"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.goodmath.org\/blog\/wp-json\/wp\/v2\/tags?post=633"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}