相信你对 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 的编程能力和理解能力。