【问题标题】:Sorting in JavaScript: Shouldn't returning a boolean be enough for a comparison function?JavaScript 中的排序:返回布尔值对于比较函数来说是否足够?
【发布时间】:2014-07-27 15:53:31
【问题描述】:

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

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

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

【问题讨论】:

    标签: javascript sorting comparison


    【解决方案1】:

    TL;DR

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

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

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

    为什么?

    因为即使b 大于a,您的比较函数也会返回false(或0,等价)。但是0 暗示这两个元素被认为是相等的——排序算法相信这一点。

    深入讲解

    JavaScript 中的比较函数

    比较函数是如何工作的?

    Array::sort method 可以采用可选的自定义比较函数作为其参数。该函数接受两个参数(通常称为ab),它应该比较它们,并且应该返回一个数字

    • > 0a 被认为大于b 并且应该排在它之后
    • == 0a 被认为等于 b 时,哪个先出现并不重要
    • < 0a 被认为小于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 spec on sortES6 spec 中的相同):

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

    如果所有值 abc(可能是相同的值)在集合S:符号a &lt;CF b表示comparefn(a,b) &lt; 0a =CF b 表示comparefn(a,b) = 0(任一符号);而a &gt;CF b 表示comparefn(a,b) &gt; 0

    当给定一对特定的值 ab 作为其两个参数时,调用 comparefn(a,b) 总是返回相同的值 v。此外,Type(v) 是数字,v 不是NaN。请注意,这意味着对于给定的一对aba &lt;CF ba =CF ba &gt;CF b 中恰好有一个为真。

    • 调用 comparefn(a,b) 不会修改 this 对象。
    • a =CF a (reflexivity)
    • 如果a =CF b,那么b =CF a (symmetry)
    • 如果a =CF bb =CF c,那么a =CF ctransitivity=CF
    • 如果a &lt;CF bb &lt;CF c,那么a &lt;CF c&lt;CF 的传递性)
    • 如果a &gt;CF bb &gt;CF c,那么a &gt;CF c&gt;CF 的传递性)

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

    呃,这是什么意思?我为什么要关心?

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

    最重要的是传递性。它说当算法比较了两个值ab,以及bc,并通过应用比较函数发现例如a = bb &lt; c,那么 可以预期 a &lt; 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
    

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

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

    因为在许多其他语言中,有些排序算法不需要three-way comparison,而只是一个布尔小于运算符。 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&gt;b)||-(a&lt;b)}

    For numbers,您可以简单地返回它们的差异,它遵守上述所有法律:

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

    如果您想反向排序,只需取适当的一个并将ab 交换。

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

    【讨论】:

    • 不错的答案;两件事情。首先,减法不能产生 NaN。其次,有关使用整数(而不是像 JS 那样使用浮点数)作为比较运算符的语言中出现问题的一些示例,请参阅ericlippert.com/2011/01/20/bad-comparisons-part-one
    • [1,1,0,2].sort(function(a, b){ return a&gt;b }) 返回[0, 1, 2, 1]。或者你只是指Opera?我还没有测试过。
    • 您的反例在 Chromium 66 中排序正确,用于查找最小反例的脚本在节点 8.11 中不会产生任何结果
    • 注意谓词形式直接转换为正确的比较器可能很有用:const comparator = (isLt) =&gt; (a, b) =&gt; isLt(a, b) ? -1 : isLt(b, a) ? 1 : 0,与['foo', 'bar', 'BAZ'].sort(comparator((a, b) =&gt; a.toLowerCase() &lt; b.toLowerCase())) 类似使用(或通过翻转符号与&gt; 一起使用。)
    • Here's another example 排序在 Internet Explorer 中不起作用。
    【解决方案2】:

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

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

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

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

    另一方面,return a &gt; 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]

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-12-13
      • 1970-01-01
      • 1970-01-01
      • 2021-11-04
      • 2011-03-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多