摘要:快速排序在解決中方法問題時,筆者沒有考慮時間復(fù)雜度的問題,使用的排序算法進(jìn)行重寫,在實(shí)際產(chǎn)品環(huán)境中引發(fā)不小的性能問題。中方法的兼容問題筆者發(fā)現(xiàn)或者中方法不生效不同瀏覽器實(shí)現(xiàn)機(jī)制差異,故判斷后進(jìn)行該方法的重寫處理,代碼如下
快速排序(update)
在解決 Sarafi 中 sort 方法問題時,筆者沒有考慮時間復(fù)雜度的問題,使用 O(n2) 的排序算法進(jìn)行重寫,在實(shí)際產(chǎn)品環(huán)境中引發(fā)不小的性能問題。
閱讀 v8 array.js 源碼(Array.js)后發(fā)現(xiàn),Chrome 在實(shí)現(xiàn) sort 方法時對小數(shù)組(length <= 10)進(jìn)行插入排序,對大數(shù)組進(jìn)行快速排序 O(nlogn),來降低該方法的時間復(fù)雜度。
快速排序的核心是不斷把原數(shù)組做切割,切割成小數(shù)組后再對小數(shù)組進(jìn)行相同的處理,這是一種典型的分治的算法設(shè)計(jì)思路,選取數(shù)組中第一個元素作為基準(zhǔn),可對其進(jìn)行簡單實(shí)現(xiàn)如下:
function QuickSort(arr, func) { if (!arr || !arr.length) return []; if (arr.length === 1) return arr; var pivot = arr[0]; var smallSet = []; var bigSet = []; for (var i = 1; i < arr.length; i++) { if (func(arr[i], pivot) < 0) { smallSet.push(arr[i]); } else { bigSet.push(arr[i]); } } return basicSort(smallSet, func).concat([pivot]).concat(basicSort(bigSet, func)); }
上面的算法創(chuàng)建一個新的數(shù)組作為計(jì)算結(jié)果,從空間使用的角度看是不經(jīng)濟(jì)的,Javascript 的快速排序算法中并沒有像上面的代碼那樣創(chuàng)建一個新的數(shù)組,而是在原數(shù)組的基礎(chǔ)上,通過交換元素位置實(shí)現(xiàn)排序,故而類似于 push、 pop、 splice 這幾個方法,sort 方法也是會修改原數(shù)組對象的。
function swap(arr, from, to) { if (from === to) return; let temp = arr[from]; arr[from] = arr[to]; arr[to] = temp; } function QuickSortWithPartition(arr, fn, from, to) { from = from === void 0 ? 0 : from; to = to === void 0 ? arr.length : to; if (from >= to - 1) { return arr; } let pivot = arr[from]; let smallIndex = from; let bigIndex = from + 1; for (; bigIndex < to; bigIndex++) { if (fn(arr[bigIndex], pivot) < 0) { smallIndex++; swap(arr, smallIndex, bigIndex); } } swap(arr, smallIndex, from); QuickSortWithPartition(arr, fn, from, smallIndex - 1); QuickSortWithPartition(arr, fn, smallIndex + 1, to); return arr; }
其中,from 是起始索引,to 是終止索引,如果這兩個參數(shù)缺失,則表示處理整個數(shù)組。
因?yàn)樯厦娴姆謪^(qū)過程中,大數(shù)分區(qū)和小數(shù)分區(qū)都是從左向右增長,其實(shí)我們可以考慮從兩側(cè)向中間遍歷,這樣能有效地減少交換元素的次數(shù)。舉個例子,假如我們有一個數(shù)組 [2, 1, 3, 1, 3, 1, 3],采用上面的分區(qū)算法一共會碰到三次比基準(zhǔn)元素小的情況,所以會發(fā)生三次交換;而如果我們換個思路,把從右往左找到小于基準(zhǔn)的元素,和從左往右找到大于基準(zhǔn)的元素交換,這個數(shù)組只需要交換一次即可完成排序(把第一個3和最后一個1交換)。
function QuickSortWithPartitionOp(arr, fn, from, to) { from = from === void 0 ? 0 : from; to = to === void 0 ? arr.length : to; if (from >= to - 1) { return arr; } let pivot = arr[from]; let smallEnd = from; let bigBegin = to - 1; while (smallEnd < bigBegin) { while (fn(arr[bigBegin], pivot) >= 0 && smallEnd < bigBegin) { bigBegin--; } while (fn(arr[smallEnd], pivot) <= 0 && smallEnd < bigBegin) { smallEnd++; } if (smallEnd < bigBegin) { swap(arr, smallEnd, bigBegin); } } swap(arr, smallEnd, from); QuickSortWithPartitionOp(arr, fn, from, smallEnd - 1); QuickSortWithPartitionOp(arr, fn, smallEnd + 1, to); return arr; }
快速排序算法平均時間復(fù)雜度是 O(nlogn),但它的最差情況下時間復(fù)雜度會增大到 O(n2),其性能好壞的關(guān)鍵就在于分區(qū)是否合理:如果每次都能平均分成相等的兩個分區(qū),那么只需要 logn 層遞歸;而如果每次分區(qū)都不合理,總有一個分區(qū)是空的,則需要 n 層迭代。
對于一個內(nèi)容隨機(jī)的數(shù)組而言,不太可能出現(xiàn)最差情況,但日常處理的數(shù)組往往并不是內(nèi)容隨機(jī)的,一種很容易的解決方案是不要選取固定位置的元素作為基準(zhǔn)元素,而是隨機(jī)從數(shù)組里挑出一個元素作為基準(zhǔn)元素,這樣可以極大概率地避免最差情況,然而這并不等于避免最差情況,特別是在數(shù)組很大的時候,更要求我們更謹(jǐn)慎地選取基準(zhǔn)元素。
三數(shù)取中(median-of-three)三數(shù)取中法是挑選基準(zhǔn)元素的一種常用方法:即挑選基準(zhǔn)元素時,先把第一個元素、最后一個元素和中間一個元素挑出來,這三個元素中大小在中間的那個元素就被認(rèn)為是基準(zhǔn)元素。
function getPivot(arr, fn, from, to) { let mid = (from + to) >> 1; if (fn(arr[from], arr[mid]) < 0) { swap(arr, from, mid); } if (fn(arr[from], arr[to]) > 0) { swap(arr, from, to); } if (fn(arr[to], arr[mid]) < 0) { swap(arr, to, mid); } return arr[from]; }
其他比較典型的取中值手段包括:
一種是平均間隔取一個元素,多個元素取中位數(shù)(即多取幾個,增加可靠性)
一種是對三數(shù)取中進(jìn)行遞歸運(yùn)算,先把大數(shù)組平均分成三塊,對每一塊進(jìn)行三數(shù)取中,會得到三個中值,再對這三個中值取中位數(shù)
v8 源碼中的基準(zhǔn)元素選取更為復(fù)雜:如果數(shù)組長度不超過1000,則進(jìn)行基本的三數(shù)取中;如果數(shù)組長度超過1000,那么 v8 的處理是除去首尾的元素,對剩下的元素每隔200左右挑出一個元素,對這些元素排序,找出中間的那個,并用這個元素跟原數(shù)組首尾兩個元素一起進(jìn)行三數(shù)取中。
針對重復(fù)元素的處理(三路劃分)設(shè)想一下,一個數(shù)組里如果所有元素都相等,基準(zhǔn)元素不管怎么選都是一樣的,那么在分區(qū)的時候,必然出現(xiàn)除基準(zhǔn)元素外的其他元素都被分到同一個分區(qū)的情況,進(jìn)入最差性能的 case。
那么對于重復(fù)元素應(yīng)該怎么處理呢?
從性能的角度,如果發(fā)現(xiàn)一個元素與基準(zhǔn)元素相同,那么它應(yīng)該被記錄下來,避免后續(xù)再進(jìn)行不必要的比較。
function QuickSortWithPartitionDump(arr, fn, from, to) { from = from === void 0 ? 0 : from; to = to === void 0 ? arr.length - 1 : to; if (from >= to) { return arr; } let pivot = getPivot(arr, fn, from, to); let smallEnd = from; let bigBegin = to; let i = from + 1; while (i <= bigBegin) { let r = fn(arr[i], pivot); if (r < 0) { swap(arr, smallEnd++, i++); } else if (r > 0) { swap(arr, i, bigBegin--); } else { i += 1; } } QuickSortWithPartitionDump(arr, fn, from, smallEnd - 1); QuickSortWithPartitionDump(arr, fn, bigBegin + 1, to); return arr; }針對小數(shù)組的優(yōu)化
對于小數(shù)組,可能使用快速排序的速度還不如平均復(fù)雜度更高的插入排序,故而出于減少遞歸深度的考慮,數(shù)組長度較小時,使用插入排序算法。
function insertSort(arr, fn, from, to) { for (let i = from; i < to; i++) { for (let j = i + 1; j < to; j++) { let t = fn(arr[i], arr[j]); let r = (typeof t === "number" ? t : t ? 1 : 0) > 0; if (r) { let tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } } } return arr; }v8 引擎額外做的優(yōu)化
快速排序如果遞歸太深的話很可以出現(xiàn)“爆棧”,上面提到的對小數(shù)組采用插入排序算法,以及采用內(nèi)省排序算法都可以減少遞歸深度,不過 v8 引擎中還做了一些不太常見的優(yōu)化:每次分區(qū)后,v8 引擎會選擇元素少的分區(qū)進(jìn)行遞歸,而將元素多的分區(qū)直接通過循環(huán)處理,無疑可以大大減小遞歸深度。
v8 引擎沒有做的優(yōu)化由于快速排序時間復(fù)雜度的不穩(wěn)定性,David Musser 于1997設(shè)計(jì)了內(nèi)省排序法(Introsort),這個算法在快速排序的基礎(chǔ)上,監(jiān)控遞歸的深度:一旦長度為 n 的數(shù)組經(jīng)過 logn 層遞歸(快速排序算法最佳情況下的遞歸層數(shù))還沒有結(jié)束的話,就認(rèn)為這次快速排序的效率可能不理想,轉(zhuǎn)而將剩余部分換用其他排序算法,通常使用堆排序算法(Heapsort,最差時間復(fù)雜度和最優(yōu)時間復(fù)雜度均為 O(nlogn))。
IOS 中 sort 方法的兼容問題筆者發(fā)現(xiàn) Safari 或者 iPhone 中 sort 方法不生效(不同瀏覽器實(shí)現(xiàn)機(jī)制差異),故判斷后進(jìn)行該方法的重寫處理,代碼如下:
;(function(w){ if(/msie|applewebkit.+safari/i.test(w.navigator.userAgent)){ var _sort = Array.prototype.sort; Array.prototype.sort = function(fn){ if(!!fn && typeof fn === "function"){ if(this.length < 2) return this; var i = 0, j = i + 1, l = this.length, tmp, r = false, t = 0; for(; i < l; i++){ for(j = i + 1; j < l; j++){ t = fn.call(this, this[i], this[j]); r = (typeof t === "number" ? t : !!t ? 1 : 0) > 0 ? true : false; if(r){ tmp = this[i]; this[i] = this[j]; this[j] = tmp; } } } return this; } else { return _sort.call(this); } }; } })(window);
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/83446.html
摘要:暫未找到完美的解決方法,各位看官發(fā)現(xiàn)了記得評論提醒一下安卓移動端瀏覽器設(shè)置無效,無法多選圖片問題該問題同樣暫未找到完美的解決方案別的現(xiàn)在一下子想不起來了。。。 從事前端開發(fā)將滿一年了,期間遇到不少問題,最坑的是一些自己不知道的坑。所以寫出來警示后人。 1. ios端的sort方法無效描述:之前做一個小程序的聊天列表的時候需要用到sort進(jìn)行列表排序。嗯,后來有用戶反應(yīng)最新回復(fù)不置頂。。...
摘要:暫未找到完美的解決方法,各位看官發(fā)現(xiàn)了記得評論提醒一下安卓移動端瀏覽器設(shè)置無效,無法多選圖片問題該問題同樣暫未找到完美的解決方案別的現(xiàn)在一下子想不起來了。。。 從事前端開發(fā)將滿一年了,期間遇到不少問題,最坑的是一些自己不知道的坑。所以寫出來警示后人。 1. ios端的sort方法無效描述:之前做一個小程序的聊天列表的時候需要用到sort進(jìn)行列表排序。嗯,后來有用戶反應(yīng)最新回復(fù)不置頂。。...
摘要:前言做項(xiàng)目的時候發(fā)現(xiàn)使用排序后的代碼,在和平臺解析的結(jié)果不一樣。而根據(jù)規(guī)范,通過可以推測出,顯然這里互相矛盾反之亦然的情況。 前言:做項(xiàng)目的時候發(fā)現(xiàn)使用sort排序后的代碼,在android和ios平臺解析的結(jié)果不一樣。showImg(https://segmentfault.com/img/bVbn0y2?w=150&h=150); 1、先從簡單的開始,大家都知道sort()函數(shù)比較...
摘要:使用進(jìn)行網(wǎng)絡(luò)請求推薦的形式進(jìn)行數(shù)據(jù)處理。這個時候自己去搜索了下,提出了兩種解決方案構(gòu)建表單數(shù)據(jù)參考這里但是這個在自己的機(jī)器上并不生效。服務(wù)端解決方案獲取里面的內(nèi)容,在中可以這樣寫這個時候就可以打印出數(shù)據(jù)了。 React Native 使用 fetch 進(jìn)行網(wǎng)絡(luò)請求,推薦Promise的形式進(jìn)行數(shù)據(jù)處理。官方的 Demo 如下: fetch(https://mywebsite.com/e...
閱讀 1303·2021-11-11 10:57
閱讀 3717·2021-09-07 10:10
閱讀 3442·2021-08-03 14:03
閱讀 3067·2019-08-30 13:45
閱讀 681·2019-08-29 11:19
閱讀 1039·2019-08-28 18:07
閱讀 3100·2019-08-26 13:55
閱讀 809·2019-08-26 12:17