Note that there are some explanatory texts on larger screens.

plurals
  1. POHas anyone seen this improvement to quicksort before?
    primarykey
    data
    text
    <h2>Handling repeated elements in previous quicksorts</h2> <p>I have found a way to handle repeated elements more efficiently in quicksort and would like to know if anyone has seen this done before. </p> <p>This method greatly reduces the overhead involved in checking for repeated elements which improves performance both with and without repeated elements. Typically, repeated elements are handled in a few different ways which I will first enumerate. </p> <p>First, there is the Dutch National Flag method which sort the array like <code>[ &lt; pivot | == pivot | unsorted | &gt; pivot]</code>. </p> <p>Second, there is the method of putting the equal elements to the far left during the sort and then moving them to the center the sort is <code>[ == pivot | &lt; pivot | unsorted | &gt; pivot]</code> and then after the sort the <code>==</code> elements are moved to the center. </p> <p>Third, the Bentley-McIlroy partitioning puts the <code>==</code> elements to both sides so the sort is <code>[ == pivot | &lt; pivot | unsorted | &gt; pivot | == pivot]</code> and then the <code>==</code> elements are moved to the middle. </p> <p>The last two methods are done in an attempt to reduce the overhead.</p> <h2>My Method</h2> <p>Now, let me explain how my method improves the quicksort by reducing the number of comparisons. I use two quicksort functions together rather than just one. </p> <p>The first function I will call <code>q1</code> and it sorts an array as <code>[ &lt; pivot | unsorted | &gt;= pivot]</code>. </p> <p>The second function I will call <code>q2</code> and it sorts the array as <code>[ &lt;= pivot | unsorted | &gt; pivot]</code>. </p> <p>Let's now look at the usage of these in tandem in order to improve the handling of repeated elements. </p> <p>First of all, we call <code>q1</code> to sort the whole array. It picks a pivot which we will further reference to as <code>pivot1</code> and then sorts around <code>pivot1</code>. Thus, our array is sorted to this point as <code>[ &lt; pivot1 | &gt;= pivot1 ]</code>. </p> <p>Then, for the <code>[ &lt; pivot1]</code> partition, we send it to <code>q1</code> again, and that part is fairly normal so let's sort the other partition first. </p> <p>For the <code>[ &gt;= pivot1]</code> partition, we send it to <code>q2</code>. <code>q2</code> choses a pivot, which we will reference to as <code>pivot2</code> from within this sub-array and sorts it into <code>[ &lt;= pivot2 | &gt; pivot2]</code>. </p> <p>If we look now at the entire array, our sorting looks like <code>[ &lt; pivot1 | &gt;= pivot1 and &lt;= pivot2 | &gt; pivot2]</code>. This looks very much like a dual-pivot quicksort. </p> <p>Now, let's return to the subarray inside of <code>q2</code> (<code>[ &lt;= pivot2 | &gt; pivot2]</code>). </p> <p>For the <code>[ &gt; pivot2]</code> partition, we just send it back to <code>q1</code> which is not very interesting. </p> <p>For the <code>[ &lt;= pivot2]</code> partition, we first check if <code>pivot1 == pivot2</code>. If they are equal, then this partition is already sorted because they are all equal elements! If the pivots aren't equal, then we just send this partition to <code>q2</code> again which picks a pivot (further <code>pivot3</code>), sorts, and if <code>pivot3 == pivot1</code>, then it does not have to sort the <code>[ &lt;= pivot 3]</code> and so on. </p> <p>Hopefully, you get the point by now. The improvement with this technique is that equal elements are handled without having to check if each element is also equal to the pivots. In other words, it uses less comparisons.</p> <p>There is one other possible improvement that I have not tried yet which is to check in <code>qs2</code> if the size of the <code>[ &lt;= pivot2]</code> partition is rather large (or the <code>[&gt; pivot2]</code> partition is very small) in comparison to the size of its total subarray and then to do a more standard check for repeated elements in that case (one of the methods listed above). </p> <h2>Source Code</h2> <p>Here are two very simplified <code>qs1</code> and <code>qs2</code> functions. They use the Sedgewick converging pointers method of sorting. They can obviously can be very optimized (they choose pivots extremely poorly for instance), but this is just to show the idea. My own implementation is longer, faster and much harder to read so let's start with this:</p> <pre><code>// qs sorts into [ &lt; p | &gt;= p ] void qs1(int a[], long left, long right){ // Pick a pivot and set up some indicies int pivot = a[right], temp; long i = left - 1, j = right; // do the sort for(;;){ while(a[++i] &lt; pivot); while(a[--j] &gt;= pivot) if(i == j) break; if(i &gt;= j) break; temp = a[i]; a[i] = a[j]; a[j] = temp; } // Put the pivot in the correct spot temp = a[i]; a[i] = a[right]; a[right] = temp; // send the [ &lt; p ] partition to qs1 if(left &lt; i - 1) qs1(a, left, i - 1); // send the [ &gt;= p] partition to qs2 if( right &gt; i + 1) qs2(a, i + 1, right); } void qs2(int a[], long left, long right){ // Pick a pivot and set up some indicies int pivot = a[left], temp; long i = left, j = right + 1; // do the sort for(;;){ while(a[--j] &gt; pivot); while(a[++i] &lt;= pivot) if(i == j) break; if(i &gt;= j) break; temp = a[i]; a[i] = a[j]; a[j] = temp; } // Put the pivot in the correct spot temp = a[j]; a[j] = a[left]; a[left] = temp; // Send the [ &gt; p ] partition to qs1 if( right &gt; j + 1) qs1(a, j + 1, right); // Here is where we check the pivots. // a[left-1] is the other pivot we need to compare with. // This handles the repeated elements. if(pivot != a[left-1]) // since the pivots don't match, we pass [ &lt;= p ] on to qs2 if(left &lt; j - 1) qs2(a, left, j - 1); } </code></pre> <p>I know that this is a rather simple idea, but it gives a pretty significant improvement in runtime when I add in the standard quicksort improvements (median-of-3 pivot choosing, and insertion sort for small array for a start). If you are going to test using this code, only do it on random data because of the poor pivot choosing (or improve the pivot choice). To use this sort you would call:</p> <pre><code>qs1(array,0,indexofendofarray); </code></pre> <h2>Some Benchmarks</h2> <p>If you want to know just how fast it is, here is a little bit of data for starters. This uses my optimized version, not the one given above. However, the one given above is still much closer in time to the dual-pivot quicksort than the <code>std::sort</code> time. </p> <p>On highly random data with 2,000,000 elements, I get these times (from sorting several consecutive datasets):</p> <pre><code>std::sort - 1.609 seconds dual-pivot quicksort - 1.25 seconds qs1/qs2 - 1.172 seconds </code></pre> <p>Where <code>std::sort</code> is the C++ Standard Library sort, the dual-pivot quicksort is one that came out several months ago by Vladimir Yaroslavskiy, and <code>qs1/qs2</code> is my quicksort implementation.</p> <p>On much less random data. with 2,000,000 elements and generated with <code>rand() % 1000</code> (which means that each element has roughly 2000 copies) the times are:</p> <pre><code>std::sort - 0.468 seconds dual-pivot quicksort - 0.438 seconds qs1/qs2 - 0.407 seconds </code></pre> <p>There are some instances where the dual-pivot quicksort wins out and I do realize that the dual-pivot quicksort could be optimized more, but the same could be safely stated for my quicksort.</p> <h2>Has anyone seen this before?</h2> <p>I know this is a long question/explanation, but have any of you seen this improvement before? If so, then why isn't it being used?</p>
    singulars
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload