{"id":97266,"date":"2017-10-23T07:00:00","date_gmt":"2017-10-23T21:00:00","guid":{"rendered":"https:\/\/blogs.msdn.microsoft.com\/oldnewthing\/?p=97266"},"modified":"2019-03-13T01:19:15","modified_gmt":"2019-03-13T08:19:15","slug":"20171023-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20171023-00\/?p=97266\/","title":{"rendered":"A closer look at the complexity analysis of finding the k&#8217;th smallest element in two sorted arrays"},"content":{"rendered":"<p>Given two sorted arrays of the same length, with unique elements, find the <var>k<\/var>th smallest element in the combined collection. One solution involves the <a HREF=\"http:\/\/typeocaml.com\/2017\/10\/19\/pearl-no-4-double-binary-search\/\">double binary search<\/a>. I&#8217;ll let you go read the article to see how it works. I&#8217;m here to dissect the complexity analysis. <\/p>\n<blockquote CLASS=\"q\"><p>Since every time we remove &frac14; elements, the complexity is <span TITLE=\"order of 2 times log N\"><var>O<\/var>(2*log(<var>N<\/var>))<\/span>, <i>i.e.<\/i>, <span TITLE=\"order of log N\"><var>O<\/var>(log(<var>N<\/var>))<\/span>. <\/p><\/blockquote>\n<p>Okay, that was pretty glib. How did we get there? <\/p>\n<p>If an algorithm reduces the problem to a smaller problem that is half the size, then the number of reduction steps needed to reduce the problem to size 1 is <span TITLE=\"ceiling of binary logarithm of N\">&lceil;lg&thinsp;<var>N<\/var>&rceil;<\/span>. More generally, if you have an algorithm where the ratio of the size of the next iteration to the size of the current iteration is <var>r<\/var>, then the number of reduction steps is <span TITLE=\"ceiling of log base 1 over r, of N\">&lceil;log<sub><var>1\/r<\/var><\/sub>&thinsp;N&rceil;<\/span>. <\/p>\n<p>In the above example, the ratio is <var>r<\/var> = &frac34;, so the number of reduction steps is <span TITLE=\"ceiling of log base four thirds of N\">&lceil;log<sub>4\/3<\/sub>&thinsp;<var>N<\/var>&rceil;<\/span>. We can <a HREF=\"https:\/\/en.wikipedia.org\/wiki\/Logarithm#Change_of_base\">change the logarithm to base 2<\/a> to arrive at <span TITLE=\"ceiling of binary logarithm of N, divided by binary logarithm of four thirds\">&lceil;(lg&thinsp;<var>N<\/var>)&divide;(lg&thinsp;(4\/3))&rceil;<\/span> &#x2248; <span TITLE=\"ceiling of two point four times binary logarithm of N\">&lceil;2.4 &times; lg&thinsp;<var>N<\/var>&rceil;<\/span>. <\/p>\n<p>This is not the same as <span TITLE=\"two times binary logarithm of N\">2 &times; lg&thinsp;<var>N<\/var><\/span> given in the original article. My guess is that the original article calculated the conversion factor incorrectly as <span TITLE=\"binary logarithm of 4\">lg&thinsp;4<\/span> = 2. <\/p>\n<p>But since this is all for order of magnitude calculations, an error in the constant factor is forgiven because order of magnitude ignores constant factors. <\/p>\n<p>But wait, does the formula even apply in the first place? <\/p>\n<p>The formula applies if the reduced problem is the same as the original problem, but with a smaller size. In the case under consideration, the starting point was two equal-sized arrays, but when we&#8217;re done, we have two unequal-sized arrays: One of the arrays shrunk in half, but the other stayed the same size. <\/p>\n<p>We used the wrong formula! <\/p>\n<p>Consider the second iteration, where we have one small array and one large array. And suppose the second iteration of the algorithm decides to throw away half of the smaller array. We did not throw away a quarter of the elements. We threw away half of the smaller array, which is one third of the total number of elements, which means that we threw away only a sixth! <\/p>\n<p>It gets worse at the third iteration: If we are unlucky and the algorithm decides to throw away half of the tiny array, we discarded only one tenth of the elements. <\/p>\n<p>So in the worst case, we get into a case of diminishing returns, where were throw away less and less from the small array and never make a dent in the big array. Does this algorithm even guarantee termination? <\/p>\n<p>In mathematics, sometimes the way to solve a problem is to convert it to a harder problem, and then solve the harder problem. In this case, the harder problem is &#8220;Given two sorted arrays of <i>possibly-unequal length<\/i>, with unique elements, find the <var>k<\/var>th smallest element in the combined collection.&#8221; <\/p>\n<p>Let <span TITLE=\"f of n comma m\"><var>f<\/var>(<var>n<\/var>, <var>m<\/var>)<\/span> represent the number of reduction steps needed to solve the problem, where <var>n<\/var> and <var>m<\/var> are the lengths of the two arrays. If you decide to throw away half of the first array, then the number of remaining steps is <span TITLE=\"f of n over 2 comma m\"><var>f<\/var>(&frac12;<var>n<\/var>, <var>m<\/var>)<\/span>. Similarly, if you decide to throw away half of the second array, then the number of remaining steps is <span TITLE=\"f of n comma m over 2\"><var>f<\/var>(<var>n<\/var>, &frac12;<var>m<\/var>)<\/span>. You now have the recursion <span TITLE=\"f of n comma m equals 1 plus the max of the value of f of n over 2 comma m or the value of f of n comma m over 2\"><var>f<\/var>(<var>n<\/var>, <var>m<\/var>) = 1 + max(<var>f<\/var>(&frac12;<var>n<\/var>, <var>m<\/var>),         <var>f<\/var>(<var>n<\/var>, &frac12;<var>m<\/var>))<\/span>. <\/p>\n<p>The solution to this recursion is <span TITLE=\"f of n comma m equals the ceiling of the binary logarithm of n plus the ceiling of the binary logarithm of m\"><var>f<\/var>(<var>n<\/var>, <var>m<\/var>) = &lceil;lg&thinsp;<var>n<\/var>&rceil; +  &lceil;lg&thinsp;<var>m<\/var>&rceil;<\/span>. <\/p>\n<p>You can think of the problem this way: You have two arrays, and based on the algorithm, you cut one in half or you cut the other in half, until both arrays are cut down to just one element. It takes <span TITLE=\"the ceiling of the binary logarithm of n\">&lceil;lg&thinsp;<var>n<\/var>&rceil;<\/span> cuts to reduce the first array and <span TITLE=\"the ceiling of the binary logarithm of m\">&lceil;lg&thinsp;<var>m<\/var>&rceil;<\/span> cuts to reduce the second array. The algorithm tells you which piece to cut next, but regardless of what order the algorithm gives, the total number of cuts is the same. <\/p>\n<p>In the original problem, <span TITLE=\"n equals m equals half of capital N\"><var>n<\/var> = <var>m<\/var> = &frac12;<var>N<\/var><\/span>. Therefore, the number of reduction steps is <span TITLE=\"the ceiling of the binary logarithm of half of capital N plus the ceiling of the binary logarithm of half of capital N which equals two less than twice the ceiling of the binary logarithm of capital N\">&lceil;lg&thinsp;&frac12;<var>N<\/var>&rceil; +  &lceil;lg&thinsp;&frac12;<var>N<\/var>&rceil; = 2&lceil;lg&thinsp;&frac12;<var>N<\/var>&rceil; = 2&lceil;lg&thinsp;<var>N<\/var>&rceil; &#8211; 2<\/span>, which is <span TITLE=\"order of log capital N\"><var>O<\/var>(lg&thinsp;<var>N<\/var>)<\/span>. <\/p>\n<p>So the answer given in the article was off by two. This doesn&#8217;t make any difference when calculating order of magnitude, but it was interesting that the incorrect calculation was so close to the correct one. <\/p>\n","protected":false},"excerpt":{"rendered":"<p>Let&#8217;s calculate it properly.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-97266","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Let&#8217;s calculate it properly.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/97266","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=97266"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/97266\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=97266"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=97266"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=97266"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}