在JavaScript中排序:不应该返回一个布尔值足够的比较函数?

Sorting in JavaScript: Shouldn't returning a boolean be enough for a comparison function?

我总是这样成功地对数组进行排序(当我不希望使用标准的词典排序时):

1
2
3
4
var arr = [] // some numbers or so
arr.sort(function(a, b) {
    return a > b;
});

现在,有人告诉我这是错误的,我需要用return a-b来代替。是真的吗?如果是,为什么?我已经测试了我的比较函数,它工作了!另外,为什么我的解决方案在出错时会如此常见?


DR

I have always successfully sorted my arrays like this

Ok.

不,你没有。却没有注意到。一个快速的反例:好的。

1
2
3
> [1,1,0,2].sort(function(a, b){ return a>b })
Array [0, 1, 2, 1]
// in Opera 12. Results may vary between sorting algorithm implementations

why?

Ok.

因为即使当b大于a时,比较函数也会返回false0。但是,0意味着这两个元素被认为是相等的——排序算法相信这一点。好的。深入解释javascript中的比较函数

How do comparison functions work?

Ok.

Array::sort方法可以采用可选的自定义比较函数作为参数。该函数接受两个参数(通常称为ab,应该进行比较,并应该返回一个数字好的。

  • a被认为大于b时,> 0应在它之后排序。
  • == 0a被认为等于b时,谁先来并不重要。
  • a被认为小于b时,< 0应在它之前排序。

如果它不返回一个数字,结果将被转换为一个数字(对于布尔值来说很方便)。返回的数字不一定是-101(尽管通常是这样)。好的。一致的顺序

为了保持一致,比较函数需要满足这个方程。好的。

1
2
3
comp(a, b) == -1 * comp(b, a)
// or, if values other than -1, 0 and 1 are considered:
comp(a, b) * comp(b, a) <= 0

如果该需求被破坏,排序将表现为未定义。好的。

引用sort上的ES5.1规范(ES6规范中的相同内容):好的。

如果comparefn不是此数组元素的一致比较函数[…],则将定义排序行为。好的。

如果集合S中的所有值abc(可能是相同的值),则函数comparefn是一组值S的一致比较函数;表示a 表示comparefn(a,b) < 0a =CF b表示comparefn(a,b) = 0ER符号);而a >CF b表示comparefn(a,b) > 0。好的。

当给定一对特定的值ab作为它的两个参数时,调用comparefn(a,b)总是返回相同的值v。另外,Type(v)是数字,v不是NaN。注意,这意味着对于给定的一对aba a =CF ba >CF b中的一个是正确的。好的。

  • 调用comparefn(a,b)不会修改此对象。
  • a =CF a(自反性)
  • 如果是a =CF b,那么b =CF a(对称)
  • 如果a =CF bb =CF c,那么a =CF c(=CF的传递性)
  • 如果a b ,那么a (的传递性)
  • 如果a >CF bb >CF c,那么a >CF c(>CF的传递性)

注:上述条件是必要和充分的,以确保comparefn将设定的S划分为等价类,并且这些等价类是完全有序的。好的。

好的。

Uh, what does this mean? Why should I care?

Ok.

排序算法需要比较数组中的项。要做一个好的和有效的工作,它不需要把每一个项目相互比较,而是需要能够对他们的订购进行推理。为了使其工作良好,自定义比较函数需要遵守一些规则。一个微不足道的问题是,一个项目a等于它本身(compare(a, a) == 0)——这是上面列表中的第一个项目(自反性)。是的,这有点数学化,但报酬很高。好的。

最重要的是传递性。它表示,当算法比较了ab两个值,以及bc两个值时,通过应用a = bb < c等比较函数,可以得出a < c也成立。这似乎只是逻辑上的,并且对于一个定义明确、一致的顺序是必需的。好的。

但是你的比较函数并没有达到这个目的。让我们看看这个例子:好的。

1
2
3
4
 function compare(a, b) { return Number(a > b); }
 compare(0, 2) == 0 // ah, 2 and 0 are equal
 compare(1, 0) == 1 // ah, 1 is larger than 0
 // let's conclude: 1 is also larger than 2

哎呀。这就是为什么当使用不一致的比较函数调用排序算法时,它会失败(在规范中,这是"依赖于实现的行为"——即不可预测的结果)。好的。

Why is the wrong solution so common?

Ok.

因为在许多其他语言中,有一些排序算法不希望进行三向比较,而只是一个小于运算符的布尔值。C++ EDCOX1〔27〕是一个很好的例子。如果需要确定相等性,只需使用交换参数应用两次。诚然,这样做效率更高且不易出错,但如果无法内联运算符,则需要对比较函数进行更多调用。好的。反例

I have tested my comparison function, and it works!

Ok.

只有靠运气,如果你尝试一些随机的例子。或者因为您的测试套件有缺陷-不正确和/或不完整。好的。

下面是我用来查找上述最小反例的小脚本:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function perms(n, i, arr, cb) {
// calls callback with all possible arrays of length n
    if (i >= n) return cb(arr);
    for (var j=0; j<n; j++) {
        arr[i] = j;
        perms(n, i+1, arr, cb);
    }
}
for (var i=2; ; i++) // infinite loop
    perms(i, 0, [], function(a) {
        if (    a.slice().sort(function(a,b){ return a>b }).toString()
             != a.slice().sort(function(a,b){ return a-b }).toString() )
            // you can also console.log() all of them, but remove the loop!
            throw a.toString();
    });

号什么比较函数是正确的?

当您需要词典排序时,完全不使用比较函数。如果需要,将对数组中的项进行字符串化。好的。

与关系运算符类似的通用比较函数可以实现为好的。

1
2
3
4
5
function(a, b) {
    if (a > b) return 1;
    if (a < b) return -1;
    /* else */ return 0;
}

通过一些技巧,可以将其缩小到等效的function(a,b){return +(a>b)||-(a。好的。

对于数字,您可以简单地返回它们的差异,这符合上述所有法律:好的。

1
2
3
function(a, b) {
    return a - b; // but make sure only numbers are passed (to avoid NaN)
}

如果你想逆向排序,只需取适当的一个,用b交换a。好的。

如果要对复合类型(对象等)进行排序,请将每个a和每个b替换为对相关属性的访问、方法调用或要排序的任何内容。好的。好啊。


sort函数需要一个函数,该函数需要两个参数ab并返回:

  • 如果a在b之前,则为负数。
  • 如果a在b之后,则为正数。
  • 如果A和B的相对顺序无关紧要,则为零

为了按升序对数字排序,return a - b将生成正确的返回值;例如:

1
2
3
4
a    b    ret
1    2    -1
3    2     1
2    2     0

另一方面,return a > b产生以下返回值:

1
2
3
4
a    b    ret      implied
1    2    false    0
3    2    true     1
2    2    false    0

在上面的例子中,sort函数被告知1和2是相同的(将1放在2之前或2放在1之前并不重要)。这将产生不正确的结果,例如(在Chrome 49中):

1
2
3
4
[5, 8, 7, 1, 2, 3, 4, 6, 9, 10, 11, 12, 13].sort(function(a, b) {
    return a > b;
});
// [4, 5, 3, 1, 2, 6, 7, 8, 9, 10, 11, 12, 13]