在 JavaScript 中排序:对于比较函数来说,返回一个布尔值是否就足够了?

IT技术 javascript sorting comparison
2021-01-09 20:02:35

我总是像这样成功地对我的数组进行排序(当我不想要标准的字典顺序时):

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

现在,有人告诉我这是错误的,我需要return a-b改为。这是真的吗?如果是,为什么?我已经测试了我的比较功能,它有效!另外,为什么我的解决方案出错时会如此普遍

2个回答

TL; 博士

我总是像这样成功地对我的数组进行排序

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

> [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

为什么?

因为你比较函数不会返回false(或0等价),即使b大于a0意味着这两个元素被认为是相等的——排序算法相信这一点。

深入讲解

JavaScript 中的比较函数

比较函数是如何工作的?

Array::sort方法可以采用可选的自定义比较函数作为其参数。该函数需要比较两个参数(通常称为aand b),并应该返回一个数字

  • > 0whena被认为大于b并且应该在它之后排序
  • == 0什么时候a被认为等于,b哪个先到并不重要
  • < 0whena被认为小于b并且应该在它之前排序

如果它不返回数字,结果将被转换为一个数字(这对布尔值很方便)。返回的数字不需要完全-101(尽管通常是)。

一致的排序

为了保持一致,比较函数需要满足等式

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

如果该要求被打破,排序将表现为未定义。

引用ES5.1 规范sort(与ES6 规范相同):

如果comparefn[...] 不是此数组元素的一致比较函数,则 sort 的行为是实现定义的。

函数comparefn是一组值的一致的比较函数S,如果所有的下面都满足所有值的要求ab以及c在所述一组(可能是相同的值)S:记号a <CF b装置comparefn(a,b) < 0; a =CF b手段comparefn(a,b) = 0(任一符号);a >CF b手段comparefn(a,b) > 0

当给定一对特定的值作为它的两个参数时,调用comparefn(a,b)总是返回相同的值此外,是数字,而不是请注意,这意味着恰好一个将是真正的一对给定的vabType(v)vNaNa <CF ba =CF ba >CF bab

  • 调用comparefn(a,b)不会修改 this 对象。
  • a =CF a反身性
  • 如果a =CF b, 那么b =CF a(对称性)
  • 如果a =CF bb =CF c,然后a =CF c传递=CF
  • 如果a <CF bb <CF c,然后a <CF c(的传递<CF
  • 如果a >CF bb >CF c,然后a >CF c(的传递>CF

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

呃,这是什么意思?我为什么要在乎?

排序算法需要将数组的项目相互比较。要做好且高效的工作,它不一定需要将每个项目相互比较,但需要能够推理它们的顺序。为了使其正常工作,自定义比较函数需要遵守一些规则。一个简单的问题是一个项目a等于它自己 ( compare(a, a) == 0) - 这是上面列表中的第一个项目(自反性)。是的,这有点数学问题,但效果很好。

最重要的是传递性。它表示,当算法比较了两个值ab,也比较bc,并通过应用比较函数发现,例如a = bb < c,那么它可以预期a < c成立。这似乎只是合乎逻辑的,并且是定义明确、一致的排序所必需的。

但是您的比较功能确实失败了让我们看看这个例子:

 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

oop。这就是排序算法在使用不一致的比较函数调用时会失败的原因(在规范中,这是“依赖于实现的行为”——即不可预测的结果)。

为什么错误的解决方案如此普遍?

因为在许多其他语言中,有些排序算法不期望三向比较,而只是一个小于运算符的布尔值。C++std::sort就是一个很好的例子。如果需要确定相等性,它将简单地使用交换的参数应用两次。诚然,这可以更有效且不易出错,但如果无法内联运算符,则需要对比较函数进行更多调用

反例

我已经测试了我的比较功能,它有效!

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

这是我用来查找上述最小反例的小脚本:

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();
    });

什么比较函数是正确的?

当您需要字典排序时,根本不使用比较函数。如有必要,数组中的项目将被字符串化。

像关系运算符一样工作的通用比较函数可以实现为

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

通过一些技巧,这可以缩小到等效的function(a,b){return +(a>b)||-(a<b)}.

对于 numbers,您可以简单地返回它们的差异,这符合上述所有规律:

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

如果要反向排序,只需取适当的一个并ab.

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

是排序在 Internet Explorer 中不起作用的另一个示例
2021-03-09 20:02:35
注意到将谓词形式直接转换为正确的比较器可能会很有用: const comparator = (isLt) => (a, b) => isLt(a, b) ? -1 : isLt(b, a) ? 1 : 0, 使用 like ['foo', 'bar', 'BAZ'].sort(comparator((a, b) => a.toLowerCase() < b.toLowerCase()))(或>通过翻转符号。)
2021-03-13 20:02:35
[1,1,0,2].sort(function(a, b){ return a>b }) [0, 1, 2, 1]或者你只是指Opera?我还没有测试过。
2021-03-17 20:02:35
不错的答案;两件事情。首先,减法不能产生 NaN。其次,有关使用整数(而不是像 JS 那样的浮点数)作为比较运算符的语言中出现问题的一些示例,请参阅ericlippert.com/2011/01/20/bad-comparisons-part-one
2021-03-26 20:02:35
您的反例在 Chromium 66 中正确排序,并且用于查找最小反例的脚本不会在节点 8.11 上产生任何结果
2021-04-07 20:02:35

sort函数需要一个需要两个参数aand的函数b,并返回:

  • 如果 a 出现b之前,则为负数
  • 如果 ab之后,则为正数
  • 如果 a 和 b 的相对顺序无关紧要,则为零

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

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

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

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

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

console.log([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]