正确使用 JS 的 sort 方法
sort() 方法对数组的元素做原地的排序, 并返回这个数组. 默认按照字符串的 Unicode 编码排序. sort 排序可能是不稳定的. 但其实 sort 方法可能并没有你想象的那么简单, 不信的话你耐心往下看看.
默认排序
sort 方法支持传入一个比较函数, 如果不传入则默认按照其字符串的 Unicode 编码排序, 因此默认情况下会出现以下情况.
|
|
解决方法就是:
|
|
pre - curr 的值有三种情况
- 小于 0 时, curr 排在 pre 后面
- 大于 0 时, curr 排在 pre 前面
- 等于 0 时, curr 和 pre 的位置不变
另外, 我们也可以对字符串进行排序:
|
|
可能?是不稳定?
因为 ECMAScript 只是制定了 sort 这个方法, 但并没有给出具体的实现方式以及是否需要稳定的要求, sort 的实现就和其他大多数的方法一样由浏览器自行制定. 不同的浏览器或者同个浏览器不同版本上 sort 方法可能是稳定的, 也可能是不稳定的.
这里结合 V8 源码分析下这个 sort 方法的内部调优过程.
在 V8 中, 会通过以下函数方法进入 innerArraySort
中
|
|
源码有点长, 可以自己看下 这里
注释已经点明了:
|
|
第一行注释表明这是一个原地排序, 第二行注释说明当数组长度小于 22 时(其实应该是 10 ?), 使用插入排序.
再看上面给出的条件语句, 则是判断有没有传入 comparefn
, 如果未传入, 则这里会去获取每两个数的字符串的 Unicode 码进行比较, 按照其字符串的 Unicode 编码实现排序.
|
|
当数组长度小于等于 10 时, 直接使用插入排序, 该算法算法复杂度为 O(n2) , 明显是不稳定的
当数组长度大于 10 小于等于 1000 时, 获取
third_index
即中位数. 这里的方法有点屌1third_index = from + ((to - from) >> 1);其实就是
from + ( to - from ) / 2
, 以后别用/2
了, 用>>1
逼格才高当数组长度大于 1000 时, 通过
GetThirdIndex
方法获取third_index
1234567891011121314151617var GetThirdIndex = function(a, from, to) {var t_array = new InternalArray();// Use both 'from' and 'to' to determine the pivot candidates.var increment = 200 + ((to - from) & 15);var j = 0;from += 1;to -= 1;for (var i = from; i < to; i += increment) {t_array[j] = [i, a[i]];j++;}t_array.sort(function(a, b) {return comparefn(a[1], b[1]);});var third_index = t_array[t_array.length >> 1][0];return third_index;}这个方法就要好好琢磨琢磨了.
关于
var increment = 200 + ((to - from) & 15)
, 这里拿to
与from
之差与 15 (2^4 -1) 相与, 为什么是这样写, 我也不太明白 ╮(╯▽╰)╭ , 但这个很明显就是得到一个划分范围.然后进行划分,
t_array
是一个二维数组, 第一维是下标, 第二维是下标对应的数组a
的值. 这里根据划分值increment
得到t_array
这个数组, 其实这个数组就是a
的划分, 记录了每个划分的第一个数的下标值和数值. 接着这个t_array
根据第二维即数值来对划分做排序, 然后取中间的划分的下标值接着的部分到
partition
前的代码, 其实就是做下图的处理以上其实是一个快速排序的优化算法 – 三数取中( median-of-three )
正如已知的,快速排序的阿克琉斯之踵在于,最差数组组合情况下会算法退化。
快速排序的算法核心在于选择一个基准
(pivot)
,将经过比较交换的数组按基准分解为两个数区进行后续递归。试想如果对一个已经有序的数组,每次选择基准元素时总是选择第一个或者最后一个元素,那么每次都会有一个数区是空的,递归的层数将达到n
,最后导致算法的时间复杂度退化为O(n²)
。因此pivot
的选择非常重要。v8采用的是
三数取中(median-of-three)
的优化:除了头尾两个元素再额外选择一个元素参与基准元素的竞争。第三个元素的选取策略大致为:
- 当数组长度小于等于1000时,选择折半位置的元素作为目标元素。
- 当数组长度超过1000时,每隔200-215个(非固定,跟着数组长度而变化)左右选择一个元素来先确定一批候选元素。接着在这批候选元素中进行一次排序,将所得的中位值作为目标元素
最后取三个元素的中位值作为
pivot
。
其实 Chrome 的快速排序也是饱受争议, 因为在此之前 JavaScript 的 sort 实现都是稳定的, Firefox 中使用的是归并排序, 归并排序是稳定的. 但至今 Chrome 还是坚持不稳定的快速排序算法 O__O “…
后面就是快速排序的算法了. 快速排序大概的步骤就是遍历数组, 将基准值插入到正确的位置中去, 然后以该位置左右划分, 以此递归进行下去.
常见误用
虽然 Chrome 和 Firefox 的 sort 方法实现不同, 但其实并没有什么影响. 但仍然有不少人会发现在 Chrome 的排序结果是错的, Firefox 则是正常的. 我没有去看 Firefox 的实现, 但是我大概知道为什么他们会出错.
其实大部分人都错在了 Chrome 下的 compareFn 要求返回值有三种情况, 正, 负和0. 而不少人则是返回 true 或 false. 看看下面这段代码, 这里是快速排序才需要进入的.
|
|
在 JavaScript 中, false >=0 和 true >=0 都是成立的. 既然如此, 那么 c02 这里就永远取第一条了. sort 方法返回值分为三种, 大于0, 小于0, 等于0. 不要再返回 true 或返回 false 了. 之所以你返回 true 或 false 也可以运行起来那只是因为 JavaScript 隐性的类型转换以及运气好, 没碰到快排而已=.=
比如以下代码在 Chrome 中的执行结果, 由于 Node 也是使用 V8, 所以 Node 也会有同样的问题
|
|
现在就可以解释了, 在 Chrome 中, 小于等于 10 的时候是插入排序, 排序正常, 但大于 10 的时候, 是快速排序, 如果函数返回值只有 true 或 false 那么就不正常了. 由于 Firefox 使用归并排序, 所以 Firefox 没有这个问题.
sort 方法还有常见的一个误用就是用于随机排序.
|
|
这并不能做到完全随机, 首先各个浏览器的 sort 方法实现就不同, 并且也不能证明其能进行随机的排序.
真的要进行完全随机的排序的话, 还是得使用洗牌算法, 它是一种等概率随机的 In-place 排列算法. 具体的实现可以自己去网上搜下~
参考资料: