Quicksort slower than Mergesort?
Asked Answered
O

15

21

I was working on implementing a quicksort yesterday, and then I ran it, expecting a faster runtime than the Mergesort (which I had also implemented). I ran the two, and while the quicksort was faster for smaller data sets <100 elements (and I did verify that it works), the mergesort became the quicker algorithm fairly quickly. I had been taught that quicksort is almost always "quicker" than mergesort, and I understand that there is some debate on this topic, but I at least expected it to be closer than this. For data sets >10000 elements, the mergesort was over 4 times faster. Is this to be expected, or is there an error in my quicksort code?

mergesort:

public static void mergeSort(int[ ] e)
{
    if (e.length <= 1) return;
    int[] first = new int[e.length/2];
    int[] second = new int[e.length - first.length];
    System.arraycopy(e, 0, first, 0, first.length);
    System.arraycopy(e, first.length, second, 0, second.length);
    mergeSort(first);
    mergeSort(second);
    System.arraycopy(merge(first, second), 0, e, 0, e.length);
}

private static int[] merge(int[] first, int[] second) {
    int iFirst = 0;
    int iSecond = 0;
    int iCombined = 0;

    int[] combined = new int[first.length + second.length];
    while(iFirst < first.length && iSecond < second.length) {
        if (first[iFirst] > second[iSecond]) {
            combined[iCombined++] = second[iSecond++];
        }
        else combined[iCombined++] = first[iFirst++];
    }
    for(; iFirst < first.length; iFirst++) {
        combined[iCombined++] = first[iFirst];
    }
    for(; iSecond < second.length; iSecond++) {
        combined[iCombined++] = second[iSecond];
    }
    return combined;
}

quicksort:

public static void quicksort(int[] a, int first, int last) {
    if (first >= last) return;

    int partitionIndex = partition(a, first, last);
    quicksort(a, first, partitionIndex - 1);
    quicksort(a, partitionIndex + 1, last);
}

public static int partition(int[] x, int first, int last) {
    int left = first;
    int right = last;
    int pivot = x[first];
    int pivotIdx = first;

    while(left <= right) {
        while(left < x.length && x[left] <= pivot) left++;
        while(right >= 0 && x[right] > pivot) right--;
        if (left <= right) {
            int temp = x[left];
            x[left] = x[right];
            x[right] = temp;
        }
    }
    pivotIdx = right;
    x[first] = x[right];
    x[pivotIdx] = pivot;
    return pivotIdx;
}
Osgood answered 31/1, 2009 at 0:20 Comment(0)
O
10

I actually just wrote a "linked-list comparative sort demo program" in C and arrived at a similar conclusion (that mergesort will beat quicksort for most uses), altho I have been told that quicksort is generally not used for linked lists anyway. I would note that the choice of pivot values is a monster factor -- my initial version used a random node as the pivot, and when I refined it a bit to take a mean of two (random) nodes, the exectution time for 1000000 records went from over 4 minutes to less than 10 seconds, putting it right on par with mergesort.

Mergesort and quicksort have the same big O best case (n*log(n)) and despite what people may try to claim, big O is really about iteration count and not comparison count. The biggest difference that can be produced between the two of them will always be to quicksort's detriment, and it involves lists that are already largely sorted or contain a large number of ties (when quicksort does better than mergesort, the difference will not be nearly so great). This is because ties or already sorted segments streamline straight through mergesort; when two split lists come back to be merged, if one list already contains all smaller values, all of the values on the left will be compared one at a time to the first element of the right, and then (since the returned lists have an internal order) no further comparisons need be done and the right is simply iterated onto the end. This is to say, the number of iterations will remain constant, but the number of comparisons is cut in half. If you are talking about actual time and are sorting strings, it's the comparisons that are expensive.

Ties and already sorted segments in quicksort can easily lead to unbalanced lists if the pivot value is not carefully determined, and the imbalanced lists (eg, one on the right, ten on the left) are what causes the slowdown. So, if you can get your quicksort to perform as well on an already sorted list as it does on a ramdomized list, you've got a good method for finding the pivot.

If you're interested, the demo program produces output like this:

[root~/C] ./a.out -1 3 
Using "", 0 records
Primary Criteria offset=128

Command (h for help, Q to quit): N
How many records? 4000000
New list is 562500.00 kb

Command (h for help, Q to quit): m

Mergesorting..............3999999 function calls
123539969 Iterations     Comparison calls: 82696100
Elapsed time: 0 min 9 sec


Command (h for help, Q to quit): S
Shuffled.

Command (h for help, Q to quit): q

Quicksorting..............4000000 function calls
190179315 Iterations     Comparison calls: 100817020
Elapsed time: 0 min 23 sec

Altho without the krazy kolors. There's some more stuff about it by me about halfway down this page.

ps. neither sort requires extra memory with the linked list.

Oriya answered 31/1, 2009 at 1:29 Comment(2)
This is an irrelevant answer, as it uses a linked-list backing storeAnabranch
You said that "Mergesort and quicksort have the same big O best case (n*log(n))" but I want to mention that Big O is strictly for upper bounding the running time (it is worst case only) Big Omega describes the lower bound (best case)Formenti
A
4

Mergesort is a lot slower for random array based data, as long as it fits in ram. This is the first time I see it debated.

  • qsort the shortest subarray first.
  • switch to insertion sort below 5-25 elements
  • do a normal pivot selection

Your qsort is very slow because it tries to partition and qsort arrays of length 2 and 3.

Anabranch answered 10/12, 2009 at 0:18 Comment(1)
Any reason why you suggest optimizing the quick sort implementation and not the merge sort implementation? Merge sort too can benefit from switching to insertion sort (see timsort as an example). By the way, many programming language implementations use optimized version of merge sort internally: Java, Python, C with GNU libc... The later even calls quick sort "the slower algorithm".Heraclitean
P
3

Previously discussed on SO: "Why is quicksort better than mergesort?"

~

Plattdeutsch answered 31/1, 2009 at 0:26 Comment(0)
J
3

One of the advantages of quicksort for relatively small array sizes is just an artifact of hardware implementation.

On arrays, quicksort can be done in-place, meaning that you're reading from and writing to the same area of memory. Mergesort, on the other hand, typically requires allocating new buffers, meaning your memory access is more spread out. You can see both of these behaviors in your example implementations.

As a result, for relatively small datasets, quicksort is more likely to get cache hits and therefore just tends to run faster on most hardware.

Mergesort is still a pretty good solution for large data sets or other data structures, like linked lists, as your experiments confirm.

Jig answered 31/1, 2009 at 1:11 Comment(0)
A
2

Based on this wikipedia article your results are expected.

Ailey answered 31/1, 2009 at 0:27 Comment(1)
@Stephan Eggermont: Can you point out the errors in John's implementation?Unemployment
W
2

Merge sort's worst case is quicksort's average case, so if you don't have a good implementation, merge sort is going to be faster overall. Getting quicksort to work fast is about avoiding sub-average cases. Choose a better pivot (median-of-3 helps) and you'll see a difference.

Weevily answered 31/1, 2009 at 0:57 Comment(1)
I do not understand the argumentation. If quicksort is O(n log(n)) on average it is because there exist sub average cases and you cannot avoid them, regardless of how you choose your pivot. Or am I overlooking something?Unemployment
B
1

I could imagine that by directly accessing the memory, using C for example, one can improve the performance of Quicksort more than it is possible with Mergesort.

Another reason is that Mergesort needs more memory because it's hard to implement it as an in-place sort.

And specifically for your implementation you could improve the choosing of the pivot, there are a lot of different algorithms to find a good pivot.

As can be seen on wikipedia, one can implement Quicksort in different ways.

Bernardinabernardine answered 31/1, 2009 at 0:36 Comment(0)
K
1

(1) There is an qsort algo, used by C qsort(), which doesn't require extra memory. This was most probably invented by Hoare. This makes qsort() fast in C.

(2) Randomizing the data before running qsort will almost always speed it up.

(3) selecting the median data for pivot may make it faster,

Kiki answered 31/1, 2009 at 1:39 Comment(1)
Even if it is called qsort() it is probably not a pure quick sort.Unemployment
K
1

This is consistent with the analysis of the algorithms. Merge-sort is guaranteed O(nlogn) for any input and for every runtime. Quicksort is best-case O(nlogn) and average case O(nlogn), but worst-case O(n^2), so the average execution will be in between O(nlogn) and O(n^2).

Quicksort is the best general case algorithm because it has low overhead, so it has good speed for values of n up to about 10000 or so and still good runtime for arbitrarily astronomical values of n. Merge-sort has the unfortunate overhead of writing a stack frame, required by every recursive call. Thus, for low values of n it has an atrociously high c in RT = cnlogn and it is not the preferred general sorting method.

Edit: Software Monkey pointed out a contradiction: Quicksort averages O(nlogn) for random input, but O(n^2) worst case. So it actually is somewhat bound by the entropy of your data -- or you could choose the pivot randomly. I might still be off a bit though.

Karns answered 31/1, 2009 at 4:42 Comment(2)
Quicksort can't be both "average case O(nlogn)" and "average ... between O(nlogn) and O(n^2)".Bruns
sorry average O(nlogn) for random input, but O(n^2) worst case So it actually is somewhat bound by the entropyKarns
T
1

If you implement heap sort as the base sorting algorithm in quick sorts worst case scenario, you achieve a theta(n log n) algorithm.

If you don't need stable sorting, and don't sort a linked list, I think that would be the fastest you could go.

Merge sort

Trilogy answered 31/1, 2009 at 9:26 Comment(0)
U
1

I ran similar tests and pure quick sort (with random choice of pivot) turned out to be much slower than merge sort for large arrays.

Choosing the pivot as the median of the first, middle and last element improved quick sort`s performance, but quick sort was still definitely worse than merge sort on large arrays (> 100000 elements).

I saw a big improvement when I implemented intro-sort, i.e. quick sort that falls back to heap sort if the recursion depth exceeds a certain threshold. My intro-sort implementation was almost as fast as my merge sort implementation. Of course, intro-sort is no longer pure quick sort as it uses heap sort to bring the complexity back to n log(n) when pure quick sort hits some bad data. I can post the results if you are interested.

Unemployment answered 15/5, 2012 at 18:8 Comment(0)
N
1

I think as long as data fits in memory, good merge sort implementation performs better than good quick sort implementation.

One of the most widely used implementations of qsort(), glibc qsort(), internally uses merge sort for most of the cases when data fits in memory. This merge sort allocates a temporary memory space used for merging, which adds some memory overhead, but most of the time, it outperforms its own internal quicksort implementation with good pivot selection and optimization. glibc only uses quicksort when the data and the temporary memory for merge sort cannot fit in memory.

I have measured performance of those two implementation on my machine with 2.1GHz CPU with several GB of RAM. The inputs are generated with pseudo-random generator, and each key is 32bit unsigned integer, which means a bit of more comparison cycles than integer comparison due to interface of comparison function.

For merge sort:

2 MB, time_diff 165.156000 ms, 78.752518 ns per byte
4 MB, time_diff 344.298000 ms, 82.087040 ns per byte
8 MB, time_diff 730.926000 ms, 87.133169 ns per byte
16 MB, time_diff 1541.215000 ms, 91.863573 ns per byte
32 MB, time_diff 3088.924000 ms, 92.057109 ns per byte
64 MB, time_diff 6262.868000 ms, 93.324006 ns per byte
128 MB, time_diff 12887.018000 ms, 96.015766 ns per byte
256 MB, time_diff 26731.597000 ms, 99.582959 ns per byte

For quick sort:

2 MB, time_diff 243.519000 ms, 116.118908 ns per byte
4 MB, time_diff 504.975000 ms, 120.395422 ns per byte
8 MB, time_diff 1075.276000 ms, 128.182888 ns per byte
16 MB, time_diff 2183.865000 ms, 130.168498 ns per byte
32 MB, time_diff 4343.993000 ms, 129.461080 ns per byte
64 MB, time_diff 8714.166000 ms, 129.851192 ns per byte
128 MB, time_diff 17881.344000 ms, 133.226395 ns per byte
256 MB, time_diff 36751.029000 ms, 136.908252 ns per byte

You can see that there are clear differences in performance between those two implementation and why mergesort is preferred over quicksort in such widely used qsort implementation. The main reason behind this difference seems to be because quick sort has 10-20% more comparisons than merge sort, due to uneven splitting at each step.

Newbold answered 21/11, 2012 at 22:37 Comment(0)
M
0

Were you datasets random enough? Were they partially sorted?

That might affect the speed of the sort...

Like for the QuickSort's partition(), you'd skip along if the numbers are in sorted order, until you find one that's not.

Mathildamathilde answered 31/1, 2009 at 0:47 Comment(0)
U
0

It might depend on what sort of data you're sorting for the testing (already ordered lists, randomized, reverse sorted). Also, quicksort will probably be faster in general if you pick a random pivot instead of using the first element.

Ungrateful answered 31/1, 2009 at 0:48 Comment(0)
S
0

For good performance of quicksort, it is important not to recurse all the way down to lists of length 1

You should consider sorting lists of 2, 3 and even 4 as nested ifs swapping if necessary. Let us know how the performance changes.

Swellhead answered 31/1, 2009 at 9:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.