算法对于前端工程师来说总有一层神秘色彩,这篇文章通过解读V8源码,带你探索Array.prototype.sort
函数下的算法实现。
来,先把你用过的和听说过的排序算法都列出来:
- 快速排序
- 冒泡排序
- 插入排序
- 归并排序
- 堆排序
- 希尔排序
- 选择排序
- 计数排序
- 桶排序
- 基数排序
- …
答题环节到了, sort 函数使用的以上哪一种算法?
如果你在网上搜索过关于 sort 源码的文章,可能会告诉你数组长度小于10用插入排序,否则用快速排序。
开始我也是这么认为的,可当我带着答案去 GitHub 验证的时候发现并非如此。
首先我并没有找到对应的 js 源码(文章说实现逻辑是用js写的),因为但新版本的V8源码已经修改,改用V8 Torque
。V8 Torque
是专门用来开发V8而创造的语言,语法类似TypeScript(再一次证明TypeScript的价值),它的编译器使用CodeStubAssembler
转换成高效的汇编代码。
简单理解起来就是创造了一个类似TypeScript的高效的高级语言,这个语言的文件后缀是tq
。
这里需要感谢 justjavac 大神指点~
其次当我开始阅读源码的时候,并没有找到使用快速排序的代码,也没有找到判断数组长度的常数值10。
所有的证据表明,之前的源码解读文章很可能已经过时。
那么最新版本的 V8 用的是什么排序算法呢?
算法解读
V8引擎在xx版本之后就舍弃了快速排序,因为它不是稳定的排序算法,在最坏情况下,时间复杂度会降级到O(n^2)。
而是采用了一种混合排序的算法:TimSort。
这种功能算法最初用于Python语言中,严格地说它t不属于以上10种排序算法中的任何一种,属于一种混合排序算法:
在数据量小的子数组中使用插入排序,然后再使用归并排序将有序的子数组进行合并排序,时间复杂度为 O(nlogn) 。
结合V8源码,具体实现步骤如下:
- 判断数组长度,小于2直接返回,不排序。
- 开始循环。
- 找出一个有序子数组,我们称之为“run”,长度为 currentRunLength 。
- 计算最小合并序列长度 minRunLength (这个值会根据数组长度动态变化,在32~64之间)。
- 比较 currentRunLength 和 minRunLength ,如果 currentRunLength >= minRunLength ,否则采用插入排序补足数组长度至 minRunLength ,将 run 压入栈 pendingRuns 中。
- 每次有新的 run 被压入 pendingRuns 时保证栈内任意3个连续的 run(run0, run1, run2)从下至上满足run0>run1+run2 && run1>run2 ,不满足的话进行调整直至满足。
- 如果剩余子数组为0,结束循环。
- 合并栈中所有 run,排序结束。
源码解读
源码路径
/thrid_party/v8/builtins/array-sort.tq
调用栈
|
|
核心源码
tq语言虽然有些不一样,但如果有TypeScript基础的话阅读起来应该不成问题。
下面重点解读3个函数的源码:
|
|
|
|
|
|
总结
下次面试前端岗位的时候,如果面试官问你算法题,你可以莞尔一笑地问他/她:
知道 Array 的 sort 函数使用了什么算法吗?TimSort要不要了解一下?
当然如果因此搞得面试官难堪而导致拿不到offer可别怪作者~
参考:
一部由众多技术专家推荐, 帮你成为具有全面能力和全局视野工程师的进阶利器—— 《了不起的JavaScript工程师》出版了! 点击下方链接即刻踏上进阶之路!
- 淘宝:https://detail.tmall.com/item.htm?id=600756390664
- 京东:https://item.jd.com/12562349.html?dist=jd
- 当当:http://product.dangdang.com/27922044.html