12

When sorting an array of numbers in JavaScript, I accidentally used < instead of the usual - -- but it still works. I wonder why?

Example:

var a  = [1,3,2,4]
a.sort(function(n1, n2){
    return n1<n2
})
// result is correct: [4,3,2,1]

And an example array for which this does not work (thanks for Nicolas's example):

[1,2,1,2,1,2,1,2,1,2,1,2]
Boann
  • 48,794
  • 16
  • 117
  • 146
Kuan
  • 11,149
  • 23
  • 93
  • 201
  • You could check it out by seeing what the numeric value of the return is. Noting that returning `0` has cross-browser implications. – Dave Newton Oct 05 '18 at 18:12
  • 2
    @usr2564301 `return n2 - n1` – Jeto Oct 05 '18 at 18:14
  • Possible duplicate of [How to sort an array of integers correctly](https://stackoverflow.com/questions/1063007/how-to-sort-an-array-of-integers-correctly) – Dexygen Oct 05 '18 at 18:18
  • @GeorgeJempty: no, after Jeto's reminder (insert forehead slap), it is correct to point this out. For this to work, every single element must be compared with every single other one. If that is *not* the case, it may be that another initial order does not get sorted correctly. And if it *does* test everything against everything else, it'd be quite a bad sorting algo -- at least not quicksort. – Jongware Oct 05 '18 at 18:19
  • @GeorgeJempty Thanks, I know the correct way to sort, I just want to know why this way works too? – Kuan Oct 05 '18 at 18:26
  • 2
    One example on which it doesn't work is `[1,2,1,2,1,2,1,2,1,2,1,2]`. I don't know if short examples can fail. – NicolasB Oct 05 '18 at 18:26
  • @NicolasB Thanks so much. This is what I need – Kuan Oct 05 '18 at 18:31
  • 1
    @NicolasB short examples can't fail on V8 (see my answer). – ggorlen Oct 05 '18 at 19:47
  • @ggorlen Yes, eventually realized what you had already posted, that different algorithms are used based on the size of the array to sort. Thanks for your answer :) – NicolasB Oct 05 '18 at 19:49
  • Possible duplicate of [Sorting in JavaScript: Shouldn't returning a boolean be enough for a comparison function?](https://stackoverflow.com/q/24080785/1048572) – Bergi May 24 '20 at 18:46

5 Answers5

9

This sort works on your input array due to its small size and the current implementation of sort in Chrome V8 (and, likely, other browsers).

The return value of the comparator function is defined in the documentation:

  • If compareFunction(a, b) is less than 0, sort a to an index lower than b, i.e. a comes first.
  • If compareFunction(a, b) returns 0, leave a and b unchanged with respect to each other, but sorted with respect to all different elements.
  • If compareFunction(a, b) is greater than 0, sort b to an index lower than a, i.e. b comes first.

However, your function returns binary true or false, which evaluate to 1 or 0 respectively when compared to a number. This effectively lumps comparisons where n1 < n2 in with n1 === n2, claiming both to be even. If n1 is 9 and n2 is 3, 9 < 3 === false or 0. In other words, your sort leaves 9 and 3 "unchanged with respect to each other" rather than insisting "sort 9 to an index lower than 3".

If your array is shorter than 11 elements, Chrome V8's sort routine switches immediately to an insertion sort and performs no quicksort steps:

// Insertion sort is faster for short arrays.
if (to - from <= 10) {
  InsertionSort(a, from, to);
  return;
}

V8's insertion sort implementation only cares if the comparator function reports b as greater than a, taking the same else branch for both 0 and < 0 comparator returns:

var order = comparefn(tmp, element);
if (order > 0) {
  a[j + 1] = tmp;
} else {
  break;
}

Quicksort's implementation, however, relies on all three comparisons both in choosing a pivot and in partitioning:

var order = comparefn(element, pivot);
if (order < 0) {
  // ...
} else if (order > 0) {
  // ...
}
// move on to the next iteration of the partition loop

This guarantees an accurate sort on arrays such as [1,3,2,4], and dooms arrays with more than 10 elements to at least one almost certainly inaccurate step of quicksort.


Update 7/19/19: Since the version of V8 (6) discussed in this answer, implementation of V8's array sort moved to Torque/Timsort in 7.0 as discussed in this blog post and insertion sort is called on arrays of length 22 or less.

The article linked above describes the historical situation of V8 sorting as it existed at the time of the question:

Array.prototype.sort and TypedArray.prototype.sort relied on the same Quicksort implementation written in JavaScript. The sorting algorithm itself is rather straightforward: The basis is a Quicksort with an Insertion Sort fall-back for shorter arrays (length < 10). The Insertion Sort fall-back was also used when Quicksort recursion reached a sub-array length of 10. Insertion Sort is more efficient for smaller arrays. This is because Quicksort gets called recursively twice after partitioning. Each such recursive call had the overhead of creating (and discarding) a stack frame.

Regardless of any changes in the implementation details, if the sort comparator adheres to standard, the code will sort predictably, but if the comparator doesn't fulfill the contract, all bets are off.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • Hence my comment about the "Note" regarding the ECMAScript standard wrt browsers. – Dave Newton Oct 05 '18 at 18:34
  • I'm not sure I follow. Which browsers are implementing the comparator return value differently than `< 0`/`0`/`> 0`? – ggorlen Oct 05 '18 at 18:44
  • That's not what MDN says, it says ECAMScript doesn't guarantee the behavior of returning `0`. It's right there in the docs you linked to. – Dave Newton Oct 05 '18 at 18:57
  • Ah, I see where you're referring. I read that differently, having impact only on sort stability, which, yes, may vary from implementation to implementation and doesn't seem to apply to this case (in other words, if all elements are unique as in OP's input, `0` should not be returned by any implementation, it'd just be wrong; OP's comparator is incorrectly calling unequal elements equal). – ggorlen Oct 05 '18 at 19:03
  • 1
    @ggorlen Thanks for the depth explanation – Kuan Oct 08 '18 at 16:47
2

If we analyze what's being done, it seems that this is mostly luck as in this case, 3 and 2 are considered to be "the same" and should be interchangeable. I suppose in such cases, the JS engines keep the original order for any values that have been deemed equal:

let a = [1, 3, 2, 4];
a.sort((n1, n2) => {
  const result = n1 < n2;
  if (result < 0) {
    console.log(`${n1} comes before ${n2}`);
  } else if (result > 0) {
    console.log(`${n2} comes before ${n1}`);
  } else {
    console.log(`${n1} comes same position as ${n2}`);
  }
  return result;
})

console.log(a);

As pointed out in the comments, this isn't guaranteed to work ([1,2,1,2,1,2,1,2,1,2,1,2] being a counter-example).

Jeto
  • 14,596
  • 2
  • 32
  • 46
2

After my initial comment, I wondered a little bit about how easy it is to find arrays for which this sorting method fails.

I ran an exhaustive search on arrays of length up to 8 (on an alphabet of size the size of the array), and found nothing. Since my (admittedly shitty) algorithm started to be too slow, I changed it to an alphabet of size 2 and found that binary arrays of length up to 10 are all sorted properly. However, for binary arrays of length 11, many are improperly sorted, for instance [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0].

// Check if 'array' is properly sorted using the "<" comparator function
function sortWorks(array) {
    let sorted_array = array.sort(function(n1, n2) {
        return n1 < n2;
    });
    for (let i=0; i<sorted_array.length-1; i++) {
        if (sorted_array[i] < sorted_array[i+1]) return false;
    }
    return true;
}

// Get a list of all arrays of length 'count' on an alphabet of size 'max_n'.
// I know it's awful, don't yell at me :'(
var arraysGenerator;
arraysGenerator = function (max_n, count) {
    if (count === 0) return [[]];
    let arrays = arraysGenerator(max_n, count-1);
    let result = [];
    for (let array of arrays) {
        for (let i = 0; i<max_n; i++) {
            result.push([i].concat(array));
        }
    }
    return result;
}

// Check if there are binary arrays of size k which are improperly sorted,
// and if so, log them
function testArraysOfSize(k) {
    let arrays = arraysGenerator(2, k);
    for (let array of arrays) {
        if (!sortWorks(array)) {
            console.log(array);
        }
    }
}

I'm getting some weird false-positives for some reason though, not sure where my mistake is.


EDIT:

After checking for a little while, here's a partial explanation on why OP's "wrong" sorting method works for lengths <=10 and for lengths >=11: it looks like (at least some) javascript implementations use InsertionSort if the array length is short (length <= 10) and QuickSort otherwise. It looks like QuickSort actively uses the "-1" outputs of the compare function while InsertionSort does not and relies only on the "1" outputs.

Source: here, all thanks to the original author.

NicolasB
  • 966
  • 5
  • 9
0

Depending on exact sort() implementation, it is likely that it never checks for -1. It is easier and faster, and it makes no difference (as sorting is not guaranteed to be stable anyway, IIRC).

If the check sort() makes internally is compareFunction(a, b) > 0, then effectively true is interpreted as a > b, and false is interpreted as a <= b. And then your result is exactly what one would expect.

Of course the key point is that for > comparison true gets covered to 1 and false to 0.

Note: this is all speculation and guesswork, I haven't confirmed it experimentally or in browser source code - but it's reasonably likely to be correct.

Frax
  • 5,015
  • 2
  • 17
  • 19
0

Sort function expects a comparator which returns a number (negative, zero, or positive).

Assuming you're running on top of V8 engine (Node.js, Chrome etc.), you can find in array implementation that the returned value is compared to 0 (yourReturnValue > 0). In such case, the return value being casted to a number, so:

  • Truthy values are converted to 1
  • Falsy values are converted to 0

So based the documentation and the above, your function will return a sorted array in descending order in your specific case, but might brake in other cases since there's no regard to -1 value.

Reuven Chacha
  • 879
  • 8
  • 20