由一道算法题引发的思考。
以前在leetcode刷题的时候遇到这道题(题目来源寻找两个有序数组的中位数)javascript
给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。
请你找出这两个有序数组的中位数,而且要求算法的时间复杂度为 O(log(m + n))。
你能够假设 nums1 和 nums2 不会同时为空。html
示例1 nums1 = [1, 3] nums2 = [2] 则中位数是 2.0 示例2 nums1 = [1, 2] nums2 = [3, 4] 则中位数是 (2 + 3)/2 = 2.5
当初作的时候没有认真审题,直接两个数组合并而后排序再取中位数,提交的时候也直接AC了。后来看解析的时候发现有人吐槽js直接用sort,耍赖皮,我才发现,原来题目要求时间复杂度为O(log(m + n))。java
那么问题来了,排序最快也要O(nlogn),那为什么不会超时呢?git
先看下我不审题的解答github
var findMedianSortedArrays = function(nums1, nums2) { let num = nums1.concat(nums2); num = num.sort((a, b) => a - b); let mid = Math.floor(num.length / 2); if (num.length % 2 === 0) { return (num[mid-1] + num[mid])/2 } else { return num[mid] } };
我这里用的是V8优化事后的sort而不是普通的快排,那么是否是证实V8的sort要比快排还要快呢?算法
这里我写了一个脚本,用来比较两个算法的运行时长segmentfault
var quickSort = function (arr) { if (arr.length <= 1) { return arr; } var pivotIndex = Math.floor(arr.length / 2); var pivot = arr.splice(pivotIndex, 1)[0]; var left = []; var right = []; for (var i = 0; i < arr.length; i++) { if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return quickSort(left).concat([pivot], quickSort(right)); }; let arr = [], brr = [], idx = 0, length = Math.floor(Math.random() * 10000000); while (idx < length) { arr[idx] = brr[idx] = Math.floor(Math.random() * length); idx++; } console.log('length===', length) console.time('quicksort') quickSort(arr) console.timeEnd('quicksort') console.time('V8_sort') brr.sort((a, b) => { return a - b }) console.timeEnd('V8_sort')
咱们能够看到结果,不管随机数组的长度如何,显然V8提供的sort是要比快排快的(可能有人吐糟个人快排有问题,快排写法取自阮一峰老师的博客,可能又有杠精要说阮一峰老师的快排是非原地快排,好的,请出门左拐,不送)数组
这里先给你们补习一下快排的原理,熟悉的同窗能够直接到下一标题。dom
①选择一个元素做为"基准"
②小于"基准"的元素,都移到"基准"的左边;大于"基准"的元素,都移到"基准"的右边。
③对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到全部子集只剩下一个元素为止。函数
如下示例取自阮一峰老师的博客快速排序(Quicksort)的Javascript实现
举例来讲,如今有一个数据集{85, 24, 63, 45, 17, 31, 96, 50},怎么对其排序呢?
第一步,选择中间的元素45做为"基准"。(基准值能够任意选择,可是选择中间的值比较容易理解。)
第二步,按照顺序,将每一个元素与"基准"进行比较,造成两个子集,一个"小于45",另外一个"大于等于45"。
第三步,对两个子集不断重复第一步和第二步,直到全部子集只剩下一个元素为止。
其中V8中的sort并非单一的一种排序方法,而是根据数组长度来选择具体的方法,当数组长度小于等于22,选择用插入排序,大于22则选择快速排序,源码中是这样写到:
// In-place QuickSort algorithm. // For short (length <= 22) arrays, insertion sort is used for efficiency.
插入排序其实没什么好说的,本文就此略过。
那么咱们重点来看V8中sort的快速排序是怎么实现的。
先看源码
if (to - from <= 10) { InsertionSort(a, from, to); return; } if (to - from > 1000) { third_index = GetThirdIndex(a, from, to); } else { third_index = from + ((to - from) >> 1); }
①当数组长度小于等于10,剩下的数组直接用插入排序
②当数组长度大于10小于等于1000时,third_index = from + ((to - from) >> 1);
③当数组长度大于1000时,每隔 200 ~ 215 个元素取一个值,而后将这些值进行排序,取中间值的下标,经过如下函数实现
var GetThirdIndex = function(a, from, to) { var t_array = new InternalArray(); // Use both 'from' and 'to' to determine the pivot candidates. var increment = 200 + ((to - from) & 15); var j = 0; from += 1; to -= 1; for (var i = from; i < to; i += increment) { t_array[j] = [i, a[i]]; j++; } t_array.sort(function(a, b) { return comparefn(a[1], b[1]); }); var third_index = t_array[t_array.length >> 1][0]; return third_index; }
这里补充一下from + ((to - from) >> 1)
和200 + ((to - from) & 15)
中的&和>>:
①&:按位与运算符“&”是双目运算符。其功能是参与运算的两数各对应的二进位相与。只有对应的两个二进位都为1时,结果位才为1。参与运算的两个数均以补码出现。
规则:
1&1=1 1&0=0 0&1=0 0&0=0
例如:
3:0000 0011 5:0000 0101 获得的结果是: 1:0000 0001 因此3 & 5 = 1
②>>:按照二进制把数字右移指定数位,符号位为正补零,符号位负补一,低位直接移除。
例如:
let a = 60; (60: 0011 1100) a >> 2 以后等于 15 (15: 0000 1111)
源码太长,咱们在这就不一行一行地过,直接贴上比较关键的代码,有兴趣的同窗能够去看github上面的源码V8 sort源码,建议从第710行开始看
if (!IS_CALLABLE(comparefn)) { comparefn = function (x, y) { if (x === y) return 0; if (%_IsSmi(x) && %_IsSmi(y)) { return %SmiLexicographicCompare(x, y); } x = TO_STRING(x); y = TO_STRING(y); if (x == y) return 0; else return x < y ? -1 : 1; }; } var InsertionSort = f
用过sort的同窗应该知道,该函数接收一个函数comparefn做为参数,若不传,则默认将元素以字符串的方式升序排序,如:
var InsertionSort = function InsertionSort(a, from, to) { for (var i = from + 1; i < to; i++) { var element = a[i]; for (var j = i - 1; j >= from; j--) { var tmp = a[j]; var order = comparefn(tmp, element); if (order > 0) { a[j + 1] = tmp; } else { break; } } a[j + 1] = element; } }; var GetThirdIndex = function(a, from, to) { var t_array = new InternalArray(); // Use both 'from' and 'to' to determine the pivot candidates. var increment = 200 + ((to - from) & 15); var j = 0; from += 1; to -= 1; for (var i = from; i < to; i += increment) { t_array[j] = [i, a[i]]; j++; } t_array.sort(function(a, b) { return comparefn(a[1], b[1]); }); var third_index = t_array[t_array.length >> 1][0]; return third_index; } var QuickSort = function QuickSort(a, from, to) { var third_index = 0; while (true) { // Insertion sort is faster for short arrays. if (to - from <= 10) { InsertionSort(a, from, to); return; } if (to - from > 1000) { third_index = GetThirdIndex(a, from, to); } else { third_index = from + ((to - from) >> 1); } // Find a pivot as the median of first, last and middle element. var v0 = a[from]; var v1 = a[to - 1]; var v2 = a[third_index]; var c01 = comparefn(v0, v1); if (c01 > 0) { // v1 < v0, so swap them. var tmp = v0; v0 = v1; v1 = tmp; } // v0 <= v1. var c02 = comparefn(v0, v2); if (c02 >= 0) { // v2 <= v0 <= v1. var tmp = v0; v0 = v2; v2 = v1; v1 = tmp; } else { // v0 <= v1 && v0 < v2 var c12 = comparefn(v1, v2); if (c12 > 0) { // v0 <= v2 < v1 var tmp = v1; v1 = v2; v2 = tmp; } } // v0 <= v1 <= v2 a[from] = v0; a[to - 1] = v2; var pivot = v1; var low_end = from + 1; // Upper bound of elements lower than pivot. var high_start = to - 1; // Lower bound of elements greater than pivot. a[third_index] = a[low_end]; a[low_end] = pivot; // From low_end to i are elements equal to pivot. // From i to high_start are elements that haven't been compared yet. partition: for (var i = low_end + 1; i < high_start; i++) { var element = a[i]; var order = comparefn(element, pivot); if (order < 0) { a[i] = a[low_end]; a[low_end] = element; low_end++; } else if (order > 0) { do { high_start--; if (high_start == i) break partition; var top_elem = a[high_start]; order = comparefn(top_elem, pivot); } while (order > 0); a[i] = a[high_start]; a[high_start] = element; if (order < 0) { element = a[i]; a[i] = a[low_end]; a[low_end] = element; low_end++; } } } if (to - high_start < low_end - from) { QuickSort(a, high_start, to); to = low_end; } else { QuickSort(a, from, low_end); from = high_start; } } };
举个例子🌰:
现有一个数组let arr= [1, 3, 9, 7, 0, 5, 2, 10, 6, 8, 4]
;
0 + ((11 - 0) >> 1)
等于5a[from]
也就是a[0]
,a[to]
也就是a[10]
和基准a[5]
三者之间比较大小,获得新的数组是[1, 3, 9, 7, 0, 4, 2, 10, 6, 8, 5]
,其中a[from] == a[0] == 1,a[from] == a[10] == 5,基准值a[5] == 4
;[1, 4, 9, 7, 0, 3, 2, 10, 6, 8, 5]
[1, 4, 2, 7, 0, 3, 9, 10, 6, 8, 5]
[1, 2, 4, 7, 0, 3, 9, 10, 6, 8, 5]
,接着重复步骤4[1, 2, 4, 3, 0, 7, 9, 10, 6, 8, 5]
[1, 2, 3, 4, 0, 7, 9, 10, 6, 8, 5]
[1, 2, 3, 0, 4, 7, 9, 10, 6, 8, 5]
V8中的sort并非一种单纯的排序方式,而是结合了插入排序以及快速排序的函数,而且针对快排作了优化。
V8 7.0 数组开始使用 TimSort 排序算法,在个人下一篇文章中会有讲述Timsort的原理本人才疏学浅,如有错误之处,请指正,一定尽快更改。