72

I know that the ECMA Script specification does not specify which algorithm to use for sorting arrays, nor does it specify whether the sort should be stable.

I've found this information for Firefox which specifies that firefox uses a stable sort.

Does anyone know about IE 6/7/8, Chrome and Safari?

Krisztián Balla
  • 19,223
  • 13
  • 68
  • 84
Boushley
  • 6,816
  • 4
  • 27
  • 29

5 Answers5

73

As of ES2019, sort is required to be stable. In ECMAScript 1st edition through ES2018, it was allowed to be unstable.

Simple test case (ignore the heading, second set of numbers should be sequential if the engine's sort is stable). Note: This test case doesn't work for some versions of Chrome (technically, of V8) that switched sorting algorithms based on the size of the array, using a stable sort for small arrays but an unstable one for larger arrays. (Details.) See the end of the question for a modified version that makes the array large enough to trigger the behavior.

IE's sort has been stable as long as I've ever used it (so IE6). Checking again in IE8 and it appears to still be the case.

And although that Mozilla page you link to says Firefox's sort is stable, I definitely say this was not always the case prior to (and including) Firefox 2.0.

Some cursory results:

  • IE6+: stable
  • Firefox < 3: unstable
  • Firefox >= 3: stable
  • Chrome < 70: unstable
  • Chrome >= 70: stable
  • Opera < 10: unstable
  • Opera >= 10: stable
  • Safari 4: stable
  • Edge: unstable for long arrays (>512 elements)

All tests on Windows.

See also: Fast stable sorting algorithm implementation in javascript

This test case (modified from here) will demonstrate the problem in V8 (for instance, Node v6, Chrome < v70) by ensuring the array has enough entries to pick the "more efficient" sort method; this is written with very old JavaScript engines in mind, so without modern features:

function Pair(_x, _y) {
    this.x = _x;
    this.y = _y;
}
function pairSort(a, b) {
    return a.x - b.x;
}
var y = 0;
var check = [];
while (check.length < 100) {
    check.push(new Pair(Math.floor(Math.random() * 3) + 1, ++y));
}
check.sort(pairSort);
var min = {};
var issues = 0;
for (var i = 0; i < check.length; ++i) {
    var entry = check[i];
    var found = min[entry.x];
    if (found) {
        if (found.y > entry.y) {
            console.log("Unstable at " + found.i + ": " + found.y + " > " + entry.y);
            ++issues;
        }
    } else {
        min[entry.x] = {x: entry.x, y: entry.y, i: i};
    }
}
if (!issues) {
    console.log("Sort appears to be stable");
}
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
Crescent Fresh
  • 115,249
  • 25
  • 154
  • 140
15

I'd like to share a trick I routinely use in C/C++ for qsort().

JS' sort() allows to specify a compare function. Create second array of the same length and fill it with increasing numbers from 0.

function stableSorted(array, compareFunction) {
  compareFunction = compareFunction || defaultCompare;
  var indicies = new Array(array.length);
  for (var i = 0; i < indicies.length; i++)
    indicies[i] = i;

This are indexes into the original array. We are going to sort the second array. Make a custom compare function.

  indicies.sort(function(a, b)) {

It will get the two elements from the second array: use them as indexes into the original arrays and compare the elements.

    var aValue = array[a], bValue = array[b];
    var order = compareFunction(a, b);
    if (order != 0)
      return order;

If elements happen to be equal, then compare their indexes to make the order stable.

   if (a < b)
     return -1;
   else
     return 1;
  });

After the sort(), the second array would contain indexes which you can use to access the elements of original array in stable sorted order.

  var sorted = new Array(array.length);
  for (var i = 0; i < sorted.length; i++)
    sorted[i] = array[indicies[i]];
  return sorted;
}

// The default comparison logic used by Array.sort(), if compareFunction is not provided:
function defaultCompare(a, b) {
  a = String(a);
  b = String(b);
  if (a < b) return -1;
  else if (a > b) return 1;
  else return 0;
}

In general, stable sort algorithms are only maturing and still require more extra memory compared to the good ol' qsort. I guess that's why very few specs mandate stable sort.

Jeremy
  • 1
  • 85
  • 340
  • 366
Dummy00001
  • 16,630
  • 5
  • 41
  • 63
10

As of V8 v7.0 and Chrome 70, our Array.prototype.sort implementation is now stable.

Previously, V8 used an unstable QuickSort for arrays with more than 10 elements. Now, V8 uses the stable TimSort algorithm.

The only major engine JavaScript engine that still has an unstable Array#sort implementation is Chakra, as used in Microsoft Edge. Chakra uses QuickSort for arrays with more than 512 elements. For smaller arrays, it uses a stable insertion sort implementation.

Demo: https://mathiasbynens.be/demo/sort-stability

Mathias Bynens
  • 144,855
  • 52
  • 216
  • 248
  • 1
    Could you briefly explain why the old implementation of V8 with QuickSort was considered unstable? Anyway, congratulations on your excellent work. – Mathiasfc Sep 04 '18 at 01:02
  • 1
    QuickSort is generally unstable because of how the partitioning works. Stable QuickSort versions exist, but they require additional memory and are not very efficient. – Mathias Bynens Sep 04 '18 at 06:04
  • 1
    They should all be guaranteed as stable in the future: https://github.com/tc39/ecma262/pull/1340 Edit: LOL OK that PR is actually yours. ^^' – lapo Dec 04 '19 at 22:24
0

In case anyone finds it useful, I had a polyfill for this that I am now removing:

const stable = (count => {
    const
        array = new Array(count),
        buckets = {};

    let i, k, v;

    for (i = 0; i < count; ++i) {
        array[i] = [Math.floor(Math.random() * 3) + 1, i + 1];  // [1..3, 1..count]
    }

    array.sort((a, b) => a[0] - b[0]);

    for (i = 0; i < count; ++i) {
        [k, v] = array[i];

        if (buckets[k] > v) {
            return false;
        }

        buckets[k] = v;
    }

    return true;
// Edge's JS engine has a threshold of 512 before it goes unstable, so use a number beyond that:
})(600);

if (!stable) {
    const
        { prototype } = Array,
        { sort } = prototype;

    Object.defineProperty(prototype, 'sort', {
        configurable : true,

        value(sortFn) {
            const
                array = this,
                len = array.length,
                temp = new Array(len);

            let i;

            for (i = len; i-- > 0; /* empty */) {
                temp[i] = i;
            }

            sortFn = sortFn || defaultSort;

            sort.call(temp, (index1, index2) => sortFn(array[index1], array[index2]) || index1 - index2);

            // we cannot do this directly into array since we may overwrite an element before putting it into the
            // correct spot:
            for (i = len; i-- > 0; /* empty */) {
                temp[i] = array[temp[i]];
            }

            for (i = len; i-- > 0; /* empty */) {
                array[i] = temp[i];
            }

            return array;
        }
    });
}
dongryphon
  • 705
  • 1
  • 5
  • 8
-2

If you are looking for a list of browsers where you should utilize a non native sorting algorithm, my suggestion is don't.

Instead do a sort sanity check when the script loads and make your decision from that.

As the spec doesn't require a particular behavior in that regard, it is not immune to later change, even within the same browser line.

You could submit a patch to http://www.browserscope.org/ to include such tests in their suite. But again, feature detection is superior to browser detection.

unomi
  • 2,642
  • 18
  • 19
  • 10
    I'm not sure if you could write a sanity check that would guarantee a stable sort. It might appear stable one moment but then be unstable the next. This could happen, for example, if sorting somehow relied on the location of JavaScript objects in memory - something that can be unpredictable. – Rich Dougherty Apr 06 '12 at 01:38
  • 1
    @RichDougherty -- I am sure you *couldn't*. You cannot prove a sorting algorithm is stable by running it! That would be like trying to prove a car is reliable by driving it once around the block. You have to analyze the algorithm and the implementation. – Michael Lorton Jul 21 '12 at 15:16
  • @Malvolio, I think we agree. I was saying that if a test passes now then it is certainly not guaranteed to pass in the future, hence the futility of doing a load-time check for stability. – Rich Dougherty Dec 11 '12 at 10:39
  • 2
    @RichDougherty -- re-reading your comments, I realize now that "not sure" was litotes. – Michael Lorton Dec 11 '12 at 16:14
  • 1
    While you are theoretically correct, in practice it's fairly easy to produce a data set and sort function that, if sorted stably, gives a very high probability that the sort algorithm is stable, especially with a larger data set. And of course, it's easy to prove that a sort is unstable. Here's an example I created: https://jsfiddle.net/1o5qgfzt/ Results display in the console. In Chrome, arrays of up to length 10 are sorted stably, and above that, unstably. – undefined Nov 18 '16 at 18:23