原来 JS 的 Array.sort 是插入排序和快速排序的集合!

相信你对 JS 数组的 sort 方法已经不陌生了。那你知道吗?sort 本质上是快速排序插入排序的集合,那么它的内部是如何实现的呢?如果你能够进入它的内部看一看源码,理解背后的设计,这对编程思维的提升是一个很好的帮助

sort 方法的基本使用

我们先简单回忆一下 sort 的基本使用,sort() 方法就地对数组的元素进行排序,并返回对相同数组的引用。默认排序是将元素转换为字符串,然后按照它们的 UTF-16 码元值升序排序。由于它取决于具体实现,因此无法保证排序的时间和空间复杂度。

js 复制代码
arr.sort([compareFunction])

compareFunction 定义排序顺序的函数。返回值应该是一个数字,其正负性表示两个元素的相对顺序。该函数使用以下参数调用:

  • 如果 compareFunction(a, b)小于 0,那么 a 会被排列到 b 之前;
  • 如果 compareFunction(a, b)等于 0,a 和 b 的相对位置不变;
  • 如果 compareFunction(a, b)大于 0,b 会被排列到 a 之前。
js 复制代码
const months = ['March', 'Jan', 'Feb', 'Dec'];
months.sort();
console.log(months);
// Expected output: Array ["Dec", "Feb", "Jan", "March"]

const array1 = [1, 30, 4, 21, 100000];
array1.sort();
console.log(array1);
// Expected output: Array [1, 100000, 21, 30, 4]

如果没有提供 compareFunction,所有非 undefined 的数组元素都会被转换为字符串,并按照 UTF-16 码元顺序比较字符串进行排序。例如"banana"会被排列到"cherry"之前。在数值排序中,9 出现在 80 之前,但因为数字会被转换为字符串,在 Unicode 顺序中"80"排在"9"之前。所有的 undefined 元素都会被排序到数组的末尾。

说完 sort 方法的对比函数,下面我们来看一下 sort 的底层实现。

sort 的底层实现

sort 方法在 V8 内部相较于其他方法而言是一个比较难的算法,对于很多边界情况结合排序算法做了反复的优化。在源码的实现上,发现一个非常有意思的实现,假设我们需要排序的数组长度为 n。

  • 当 n <= 10 时,采用插入排序
  • 当 n > 10 的时候,采用了快速排序

并且针对当 n > 10 的时候,对于快速排序中位数的获取有一定的处理技巧:

  • 当 10 < n <= 1000 时,采用中间元素作为中位数
  • 当 n > 1000,隔 200 ~ 215 个元素挑出一个元素,放到一个新数组中,然后对它排序,找到中间位置的数,以此作为中位数

源码🔗

n <= 10:插入排序

当 n <= 10 时采用插入排序;插入排序算法描述的是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入,从而达到排序的效果。

这里为什么 n <= 10 时为什么要用插入排序了?为什么不用合并排序或者快速排序?毕竟在时间复杂度上合并排序和快速排序都是 O(nlogn) 级别。

我个人的思考是,在实际情况中,当 n 足够小的时候,合并排序或者快速排序 nlogn 的优势会越来越小。倘若插入排序的 n 足够小,那么就会超过前二者。

而事实上正是如此,插入排序经过优化以后,对于小数据集的排序会有非常优越的性能,很多时候甚至会超过快排或者合并。因此,对于很小的数据量,应用插入排序是一个非常不错的选择。

n > 10: 快速排序

快速排序的基本思想是通过一趟排序,将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可以分别对这两部分记录继续进行排序,以达到整个序列有序。

快速排序的核心思想是"三路切分" ,"三路切分" 曾是 EMC 面试中的常客,这个名词听起来很高大上,但是简单来说就是将数组切分成三部分。在快速排序的时候,通过一个整数 x 将数组切分成小于、等于、大于三部分。实现时间复杂度 O(N),空间复杂度 O(1) 。对于快速排序而言,通过一个整数 x 将数组切分成:

  • 小于 x 部分;
  • 等于 x 的部分;
  • 大于 x 的部分;

10 < n <= 1000:直接取中点作为三路切分中位数

当 0 < n <= 1000,直接取中点作为三路切分中位数。

n > 1000:挑选元素排序之后取中位数

这里为什么要这么大费周章的去取中位数了?

这其实就是对坏情况的一个优化,我们假设取到的中位数每次都是最小元素或者最大元素,那么在进行排序时,就会出现要么一边是小于中位数的元素,要么另一边是大于中位数的元素。就会有一边是空的。如果这么排下去,递归的层数就达到了 n , 而每一层的复杂度是 O(n),因此快排这时候会退化成 O(n^2) 级别。

所以这里大费周章的去取中位数,目的就是让中位数元素尽可能地处于数组的中间位置,让最大或者最小的情况尽可能少。

总结

整体来看,sort 方法是快速排序插入排序的集合。横向对比快速排序插入排序 当 n 足够小的时候,插入排序的时间复杂度为 O(n) 要优于快速排序的 O(nlogn),所以 V8 在实现 JS sort 时,数据量较小的时候会采用了插入排序。

而当数据量 > 10 的时候,就采用了快速排序,时间复杂度 O(nlogn) 非常具有优势。希望带你了解了 sort 的底层源代码的实现逻辑,从而整体提升 JS 的编程能力和理解能力。

参考

相关推荐
蒟蒻小袁6 分钟前
力扣面试150题--被围绕的区域
leetcode·面试·深度优先
Qian Xiaoo13 分钟前
前后端分离开发 和 前端工程化
前端
掘金安东尼13 分钟前
字节-Trae、阿里-通义灵码、腾讯-CodeBuddy,为什么都在“卷”AI编码?
面试·llm·github
要加油哦~27 分钟前
vue · 插槽 | $slots:访问所有命名插槽内容 | 插槽的使用:子组件和父组件如何书写?
java·前端·javascript
先做个垃圾出来………36 分钟前
split方法
前端
前端Hardy1 小时前
HTML&CSS:3D图片切换效果
前端·javascript
spionbo2 小时前
Vue 表情包输入组件实现代码及完整开发流程解析
前端·javascript·面试
全宝2 小时前
✏️Canvas实现环形文字
前端·javascript·canvas
lyc2333332 小时前
鸿蒙Core File Kit:极简文件管理指南📁
前端