讲下 V8 sort 的大概思路,并手写一个 sort 的实现

2024-01-02 20:18

本文主要是介绍讲下 V8 sort 的大概思路,并手写一个 sort 的实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

以上是常见的几种排序算法,首先思考一下, Array.prototype.sort() 使用了上面的那种算法喃?

Array.prototype.sort()

sort() 方法用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的

— MDN

const array = [1, 30, 4, 21, 100000];
array.sort();
console.log(array);
// [1, 100000, 21, 30, 4]const numbers = [4, 2, 5, 1, 3];
numbers.sort((a, b) => a - b);
console.log(numbers)
// [1, 2, 3, 4, 5]

V8 种的 Array.prototype.sort()

关于 Array.prototype.sort() ,ES 规范并没有指定具体的算法,在 V8 引擎中, 7.0 版本之前 ,数组长度小于10时, Array.prototype.sort() 使用的是插入排序,否则用快速排序。

在 V8 引擎 7.0 版本之后 就舍弃了快速排序,因为它不是稳定的排序算法,在最坏情况下,时间复杂度会降级到 O(n2)。

于是采用了一种混合排序的算法:TimSort

这种功能算法最初用于Python语言中,严格地说它不属于以上10种排序算法中的任何一种,属于一种混合排序算法:

在数据量小的子数组中使用插入排序,然后再使用归并排序将有序的子数组进行合并排序,时间复杂度为 O(nlogn)

什么是 TimSort ?

在 解答 v8 sort 源码前,我们先看看 TimSort 具体是如何实现的,帮助我们阅读源码

Timsort 是 Tim Peter 在 2001 年为 Python 语言特意创造的,主要是 基于现实数据集中存在者大量的有序元素(不需要重新排序) 。 Timsort 会遍历所有数据,找出数据中所有有序的分区(run),然后按照一定的规则将这些分区(run)归并为一个。

具体过程为:

  • 扫描数组,并寻找所谓的 _runs_ ,一个 run 可以认为是已经排序的小数组,也包括以逆向排序的,因为这些数组可以简单地翻转(reverse)就成为一个run
  • 确定最小 run 长度,小于的 run 会通过 插入排序 归并成长度高于最小长度的 run
  • 反复归并一些相邻 run ,过程中避免归并长度相差很大的片段,直至整个排序完成

如何避免归并长度相差很大 run 呢?在 Timsort 排序过程中,会存在一个栈用于记录每个 run 的起始索引位置与长度, 依次将 run 压入栈中,若栈顶 A 、B、C 的长度

  • |C| > |B| + |A|
  • |B| > |A|

在上图的例子中,因为 | A |> | B | ,所以B被合并到了它前后两个runs(A、C)中较小的一个 | A | ,然后 | A | 再与 | C | 。 依据这个法则,能够尽量使得大小相同的 run 合并,以提高性能。注意Timsort是稳定排序故只有相邻的 run 才能归并。

所以,对于已经排序好的数组,会以 O(n) 的时间内完成排序,因为这样的数组将只产生单个 run ,不需要合并操作。最坏的情况是 O(n log n) 。这样的算法性能参数,以及 Timsort 天生的稳定性是我们最终选择 Timsort 而非 Quicksort 的几个原因。

手写一个 Array.prototype.sort() 实现

了解的 Timsort 的基本思想与排序过程后,我们手写一个简易版的 Timsort :

// 顺序合并两个小数组left、right 到 result
function merge(left, right) {let result = [],ileft = 0,iright = 0while(ileft < left.length && iright < right.length) {if(left[ileft] < right[iright]){result.push(left[ileft ++])} else {result.push(right[iright ++])}}while(ileft < left.length) {result.push(left[ileft ++])}while(iright < right.length) {result.push(right[iright ++])}return result
}// 插入排序
function insertionSort(arr) {let n = arr.length;let preIndex, current;for (let i = 1; i < n; i++) {preIndex = i - 1;current = arr[i];while (preIndex >= 0 && arr[preIndex] > current) {arr[preIndex + 1] = arr[preIndex];preIndex--;}arr[preIndex + 1] = current;}return arr;
}// timsort
function timsort(arr) {// 空数组或数组长度小于 2,直接返回if(!arr || arr.length < 2) return arrlet runs = [], sortedRuns = [],newRun = [arr[0]],length = arr.length// 划分 run 区,并存储到 runs 中,这里简单的按照升序划分,没有考虑降序的runfor(let i = 1; i < length; i++) {if(arr[i] < arr[i - 1]) {runs.push(newRun)newRun = [arr[i]]} else {newRun.push(arr[i])}if(length - 1 === i) {runs.push(newRun)break}}// 由于仅仅是升序的run,没有涉及到run的扩充和降序的run,因此,其实这里没有必要使用 insertionSort 来进行 run 自身的排序for(let run of runs) {insertionSort(run)}// 合并 runssortedRuns = []for(let run of runs) {sortedRuns = merge(sortedRuns, run)}return sortedRuns
}// 测试
var numbers = [4, 2, 5, 1, 3]
timsort(numbers)
// [1, 2, 3, 4, 5]

简易版的,完整的实现可以查看 v8 array-sort 实现,下面我们就来看一下

v8 中的 Array.prototype.sort() 源码解读

即 TimSort 在 v8 中的实现,具体实现步骤如下:

  1. 判断数组长度,小于2直接返回,不排序
  2. 开始循环
  3. 找出一个有序子数组,我们称之为 “run” ,长度 currentRunLength
  4. 计算最小合并序列长度 minRunLength (这个值会根据数组长度动态变化,在32~64之间)
  5. 比较 currentRunLength 和 minRunLength ,如果 currentRunLength >= minRunLength ,否则采用插入排序补足数组长度至 minRunLength ,将 run 压入栈 pendingRuns 中
  6. 每次有新的 run 被压入 pendingRuns 时保证栈内任意 3 个连续的 run(run0, run1, run2)从下至上满足run0 > run1 + run2 && run1 > run2 ,不满足的话进行调整直至满足
  7. 如果剩余子数组为 0 ,结束循环
  8. 合并栈中所有 run,排序结束
核心源码解读

下面重点解读 3 个核心函数:

  • ComputeMinRunLength :用来计算 minRunLength
  • CountAndMakeRun :计算第一个 run 的长度
  • MergeCollapse :调整 pendingRuns ,使栈长度大于 3 时,所有 run 都满足 run[n]>run[n+1]+run[n+2]run[n+1]>run2[n+2]
// 计算最小合并序列长度 minRunLength
macro ComputeMinRunLength(nArg: Smi): Smi {let n: Smi = nArg;let r: Smi = 0;  // Becomes 1 if any 1 bits are shifted off.assert(n >= 0);// 如果小于 64 ,则返回n(该值太小,无法打扰那些奇特的东西)// 否则不断除以2,得到结果在 32~64 之间while (n >= 64) {r = r | (n & 1);n = n >> 1;}const minRunLength: Smi = n + r;assert(nArg < 64 || (32 <= minRunLength && minRunLength <= 64));return minRunLength;
}
// 计算第一个 run 的长度
macro CountAndMakeRun(implicit context: Context, sortState: SortState)(lowArg: Smi, high: Smi): Smi {assert(lowArg < high);// 这里保存的才是我们传入的数组数据const workArray = sortState.workArray;const low: Smi = lowArg + 1;if (low == high) return 1;let runLength: Smi = 2;const elementLow = UnsafeCast<JSAny>(workArray.objects[low]);const elementLowPred = UnsafeCast<JSAny>(workArray.objects[low - 1]);// 调用比对函数来比对数据let order = sortState.Compare(elementLow, elementLowPred);// TODO(szuend): Replace with "order < 0" once Torque supports it.//               Currently the operator<(Number, Number) has return type//               'never' and uses two labels to branch.const isDescending: bool = order < 0 ? true : false;let previousElement: JSAny = elementLow;// 遍历子数组并计算 run 的长度for (let idx: Smi = low + 1; idx < high; ++idx) {const currentElement = UnsafeCast<JSAny>(workArray.objects[idx]);order = sortState.Compare(currentElement, previousElement);if (isDescending) {if (order >= 0) break;} else {if (order < 0) break;}previousElement = currentElement;++runLength;}if (isDescending) {ReverseRange(workArray, lowArg, lowArg + runLength);}return runLength;
}
// 调整 pendingRuns ,使栈长度大于3时,所有 run 都满足 run[n]>run[n+1]+run[n+2] 且 run[n+1]>run2[n+2]
transitioning macro MergeCollapse(context: Context, sortState: SortState) {const pendingRuns: FixedArray = sortState.pendingRuns;// Reload the stack size because MergeAt might change it.while (GetPendingRunsSize(sortState) > 1) {let n: Smi = GetPendingRunsSize(sortState) - 2;if (!RunInvariantEstablished(pendingRuns, n + 1) ||!RunInvariantEstablished(pendingRuns, n)) {if (GetPendingRunLength(pendingRuns, n - 1) <GetPendingRunLength(pendingRuns, n + 1)) {--n;}MergeAt(n); // 将第 n 个 run 和第 n+1 个 run 进行合并} else if (GetPendingRunLength(pendingRuns, n) <=GetPendingRunLength(pendingRuns, n + 1)) {MergeAt(n); // 将第 n 个 run 和第 n+1 个 run 进行合并} else {break;}}
}

最后

本文首发自「三分钟学前端」,回复「交流」自动加入前端三分钟进阶群,每日一道编程算法面试题(含解答),助力你成为更优秀的前端开发!

这篇关于讲下 V8 sort 的大概思路,并手写一个 sort 的实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/563534

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

usaco 1.3 Mixing Milk (结构体排序 qsort) and hdu 2020(sort)

到了这题学会了结构体排序 于是回去修改了 1.2 milking cows 的算法~ 结构体排序核心: 1.结构体定义 struct Milk{int price;int milks;}milk[5000]; 2.自定义的比较函数,若返回值为正,qsort 函数判定a>b ;为负,a<b;为0,a==b; int milkcmp(const void *va,c

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、