为什么用 < 对 JS 数字数组进行排序?

IT技术 javascript
2021-03-07 07:04:16

在 JavaScript 中对数字数组进行排序时,我不小心使用了<而不是通常的---但它仍然有效我想知道为什么?

例子:

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

以及一个不起作用的示例数组(感谢 Nicolas 的示例):

[1,2,1,2,1,2,1,2,1,2,1,2]
5个回答

这种排序适用于您的输入数组,因为它的尺寸很小并且当前sort在 Chrome V8(以及其他浏览器)中实现。

比较器函数的返回值在文档中定义

  • 如果compareFunction(a, b)小于 0,则排序a小于的索引 b,即排a在第一位。
  • 如果compareFunction(a, b)返回0,休假ab相对于彼此不变,但对于所有不同的元素进行排序。
  • 如果compareFunction(a, b)大于 0,则排序b到小于 的索引a,即排b在第一位。

但是,您的函数返回二进制trueor false与数字相比,它们的计算结果分别10这有效地将与 中的比较n1 < n2集中起来n1 === n2,声称两者是偶数。如果n1是 9 并且n2是 3,9 < 3 === false或者0换句话说,您的排序使 9 和 3“彼此保持不变”,而不是坚持“将 9 排序为低于 3 的索引”。

如果您的数组少于 11 个元素,Chrome V8 的排序例程会立即切换插入排序并且不执行快速排序步骤:

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

V8 的插入排序实现只关心比较器函数是否报告b大于aelse对两者0< 0比较器返回采用相同的分支

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

然而,Quicksort 的实现在选择主元和分区时依赖于所有三个比较:

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

这保证了对诸如[1,3,2,4]、 和 之类的数组进行准确排序,并且将具有 10 个以上元素的数组注定到至少一个几乎肯定不准确的快速排序步骤。


2019年 7 月 19 日更新:由于本答案中讨论的 V8 (6) 版本,V8 的数组排序的实现移至7.0 中的Torque / Timsort,如本博客文章中所述,并且插入排序在长度为 22 或更少的数组上调用.

上面链接的文章描述了 V8 排序在问题发生时的历史情况:

Array.prototype.sortTypedArray.prototype.sort依赖于用 JavaScript 编写的相同 Quicksort 实现。排序算法本身相当简单:基础是快速排序,带有用于较短数组(长度 < 10)的插入排序回退。当快速排序递归达到 10 的子数组长度时,也使用插入排序回退。插入排序对于较小的数组更有效。这是因为 Quicksort 在分区后被递归调用两次。每个这样的递归调用都有创建(和丢弃)堆栈帧的开销。

不管实现细节有任何变化,如果排序比较器遵守标准,代码将按可预测的方式排序,但如果比较器不履行合同,则所有赌注都将结束。

@ggorlen 感谢您的深入解释
2021-04-28 07:04:16
这不是 MDN 所说的,它说 ECAMScript 不保证返回0. 它就在您链接的文档中。
2021-05-01 07:04:16
啊,我明白你指的是哪里了。我读的是不同的,只对排序稳定性有影响,是的,这可能因实现而异,似乎不适用于这种情况(换句话说,如果所有元素在 OP 的输入中都是唯一的,0则不应该是由任何实现返回,它只是错误的;OP 的比较器错误地将不相等的元素称为相等)。
2021-05-05 07:04:16
因此,我对有关 ECMAScript 标准 wrt 浏览器的“注释”发表评论。
2021-05-11 07:04:16
我不确定我是否遵循。哪些浏览器比不同实施比较返回值< 0/ 0/ > 0
2021-05-11 07:04:16

在我最初的评论之后,我有点想知道找到这种排序方法失败的数组是多么容易。

我对长度最大为 8 的数组(在大小为数组大小的字母表上)进行了详尽的搜索,但一无所获。由于我的(无可否认的糟糕)算法开始变得太慢,我将其更改为大小为 2 的字母表,并发现长度不超过 10 的二进制数组都已正确排序。但是,对于长度为 11 的二进制数组,许多排序不正确,例如[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);
        }
    }
}

不过,由于某种原因,我收到了一些奇怪的误报,不确定我的错误在哪里。


编辑:

在检查了一会儿之后,这里是关于为什么 OP 的“错误”排序方法适用于长度 <=10 和长度 >=11 的部分解释:如果数组长度很短,看起来(至少一些)javascript 实现使用 InsertionSort (长度 <= 10)和 QuickSort 否则。看起来 QuickSort 主动使用比较函数的“-1”输出,而 InsertionSort 不并且仅依赖于“1”输出。

来源:这里,感谢原作者。

如果我们分析正在做的事情,似乎这主要是运气,因为在这种情况下,3 和 2 被认为是“相同的”,应该可以互换。我想在这种情况下,JS 引擎会保留任何被认为相等的值的原始顺序:

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

正如评论中指出的那样,这不能保证有效([1,2,1,2,1,2,1,2,1,2,1,2]作为一个反例)。

根据具体的sort()实现,它很可能永远不会检查-1. 它更容易和更快,并且没有任何区别(因为无论如何都不能保证排序是稳定的,IIRC)。

如果检查sort()使内部为 is compareFunction(a, b) > 0,则有效地true被解释为a > b,并且false被解释为a <= b然后你的结果正是人们所期望的。

当然,关键是为了>比较true,覆盖到 1 和false到 0。

注意:这都是推测和猜测,我还没有通过实验或浏览器源代码证实它 - 但它很有可能是正确的。

Sort 函数需要一个比较器,它返回一个数字(负数、零或正数)。

假设您在 V8 引擎(Node.js、Chrome 等)之上运行,您可以在数组实现中发现返回值与 0 ( yourReturnValue > 0)进行比较在这种情况下,返回值被转换为一个数字,所以:

  • 真值转换为 1
  • 假值被转换为 0

因此,根据文档和上述内容,您的函数将在您的特定情况下按降序返回一个排序数组,但在其他情况下可能会停止,因为不考虑 -1 值。