原来 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 的编程能力和理解能力。

参考

相关推荐
wakangda18 分钟前
React Native 集成原生Android功能
javascript·react native·react.js
吃杠碰小鸡21 分钟前
lodash常用函数
前端·javascript
emoji11111131 分钟前
前端对页面数据进行缓存
开发语言·前端·javascript
泰伦闲鱼34 分钟前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
m0_7482500338 分钟前
Web 第一次作业 初探html 使用VSCode工具开发
前端·html
一个处女座的程序猿O(∩_∩)O43 分钟前
vue3 如何使用 mounted
前端·javascript·vue.js
m0_748235951 小时前
web复习(三)
前端
User_undefined1 小时前
uniapp Native.js原生arr插件服务发送广播到uniapp页面中
android·javascript·uni-app
AiFlutter1 小时前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter
麦兜*1 小时前
轮播图带详情插件、uniApp插件
前端·javascript·uni-app·vue