由一道算法題引發的思考。
以前在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的原理本人才疏學淺,如有錯誤之處,請指正,一定儘快更改。