在 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]
在 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]
这种排序适用于您的输入数组,因为它的尺寸很小并且当前sort
在 Chrome V8(以及其他浏览器)中实现。
比较器函数的返回值在文档中定义:
- 如果
compareFunction(a, b)
小于 0,则排序a
到小于的索引b
,即排a
在第一位。- 如果
compareFunction(a, b)
返回0,休假a
和b
相对于彼此不变,但对于所有不同的元素进行排序。- 如果
compareFunction(a, b)
大于 0,则排序b
到小于 的索引a
,即排b
在第一位。
但是,您的函数返回二进制true
or false
,与数字相比,它们的计算结果分别为1
或0
。这有效地将与 中的比较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
大于a
,else
对两者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.sort
并TypedArray.prototype.sort
依赖于用 JavaScript 编写的相同 Quicksort 实现。排序算法本身相当简单:基础是快速排序,带有用于较短数组(长度 < 10)的插入排序回退。当快速排序递归达到 10 的子数组长度时,也使用插入排序回退。插入排序对于较小的数组更有效。这是因为 Quicksort 在分区后被递归调用两次。每个这样的递归调用都有创建(和丢弃)堆栈帧的开销。
不管实现细节有任何变化,如果排序比较器遵守标准,代码将按可预测的方式排序,但如果比较器不履行合同,则所有赌注都将结束。
在我最初的评论之后,我有点想知道找到这种排序方法失败的数组是多么容易。
我对长度最大为 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。
注意:这都是推测和猜测,我还没有通过实验或浏览器源代码证实它 - 但它很有可能是正确的。