其實本人最怕的就是演算法,大學演算法課就感覺老師在講天書,而且對於前端來說,演算法在實際的應用中實在是很有限。畢竟演算法要依靠大量的資料為基礎才能發揮出演算法的效率,就瀏覽器那效能,......是吧,退一萬步說,真的有人把這大量的資料處理業務放到前端,那我只能說這是團隊和架構師的失職,不說頁面應用能不能載入出來,等你靠前端算出來,使用者早就跑了。所以,就目前而言,絕大部分的演算法使用場景都不在前端,就那麼些資料量放在那,前端使用演算法除了加重程式碼邏輯沒有更多的好處。當然話又說回來了,我也知道這是個好東西,所以我也會去了解一點。這裡就不說什麼高深的演算法了,先總結下相對簡單的排序演算法吧,以下均為js實現。
排序演算法
1.TimSort演算法
看到這個演算法名,大多數前端er都不知道這是什麼演算法吧,但是我要是說這個是實現大名鼎鼎v8引擎的sort()的核心演算法,你們就應該恍然大悟了吧。舊版的排序sort()原理大家應該很熟悉了,陣列長度小於10用插入排序,否則使用快速排序。舊版的排序sort()原始碼地址:https://github.com/v8/v8/blob/5.9.221/src/js/array.js#L996
而新版的排序sort()原始碼使用Torque語言編寫,原始碼備註是基於python的某一版本TimSort演算法實現的,大致原理是歸併排序和插入排序的混合排序演算法:針對現實中需要排序的資料分析看,大多資料通常是有部分已經排好序的資料塊,Timsort 稱這些已經排好序(不管是升序還是降序)的資料塊為 “run”。又因為在合併序列的時候,如果run的數量等於或者略小於2
的冪次方的時候,效率是最高的,所以run要有一定的約束,於是根據序列長度定義了一個minrun,如果原始的run小於minrun的長度,用插入排序擴充run,最後將這些run用歸併排序合併序列,得到最後的run就是排序後的結果。有興趣的可以瞭解下原始碼,反正我看不懂,說到這,突然開始懷念舊版原始碼了。新版的排序sort()原始碼地址:https://github.com/v8/v8/blob/master/third_party/v8/builtins/array-sort.tq。
大多數前端排序場景用這個方法就已經足夠了,預設排序順序(沒有攜帶引數)是在將元素轉換為字串後,然後比較按照它們的UTF-16字元編碼的順序進行排序,如果要比較數字的話,就需要傳入引數compareFunction。
使用程式碼短小精悍,如下:
var arr = [11,8,5,6,3,10,7,8,2]; function compareNumbers(a,b){ return a - b; } console.log(arr.sort());//[10, 11, 2, 3, 5, 6, 7, 8, 8] console.log(arr.sort(compareNumbers));//[2, 3, 5, 6, 7, 8, 8, 10, 11]
2.氣泡排序演算法
這個可以說是最基礎的或是最常見的了吧,尤其是其形象的命名“冒泡”深入人心,顧名思義,經過不斷的交換,最大的數會像水中的泡泡一樣慢慢往上浮動,直至頂端。它會在未排序佇列內,依次比較兩個相鄰的元素,把較大的元素放置於更靠頂端的位置。
其實現程式碼如下:
function bubbleSort(array) { var length = array.length; for(var i = length - 1; i > 0; i--) { for(var j = 0; j < i; j++) { if(array[j] > array[j+1]) { var temp = array[j]; array[j] = array[j+1]; array[j+1] = temp; } } console.log(array); } return array; } var arr = [11,8,5,6,3,10,7,8,2]; var result = bubbleSort(arr);
再放上一張形象的排序流程圖,搭配上面的log圖食用更佳。
3.選擇排序演算法
在未排序佇列內,選擇其中最小的元素,然後和第一個元素進行位置互換,以此類推,直到所有元素排序完畢。
其實現程式碼如下:
function selectionSort(array) { var length = array.length; for(var i = 0; i < length; i++) { var min = array[i]; var index = i; for(var j = i + 1; j < length; j++) { if(array[j] < min) { min = array[j]; index = j; } } if(index != i) { var temp = array[i]; array[i] = array[index]; array[index] = temp; } console.log(array); } return array; } var arr = [11,8,5,6,3,10,7,8,2]; var result = selectionSort(arr);
排序流程圖:
4.插入排序演算法
這個演算法打過鬥地主或是跑的快的應該很熟悉,我這樣說就明白了:就是抓到牌之後,我們會先展開牌,然後先把第一張放到左邊(排序佇列),然後把第二張(可以看為未排序佇列第一張)從右邊(未排序佇列)再拿到左邊去按從小到大進行排序,放到合適的位置。恩,形象的丫批。上文也說到了,這是舊版的排序sort()在陣列長度小於10的情況下采用的排序演算法。
其實現程式碼如下:
function insertionSort(array) { var length = array.length; for(var i = 0; i < length - 1; i++) { var insert = array[i+1]; var index = i + 1; for(var j = i; j >= 0; j--) { if(insert < array[j]) { array[j+1] = array[j]; index = j; } } array[index] = insert; console.log(array); } return array; } var arr = [11,8,5,6,3,10,7,8,2]; var result = insertionSort(arr);
排序流程圖:
5.希爾排序演算法
希爾排序演算法是以其設計者shell的名字命名的排序演算法,又稱縮小增量排序,算是個插入排序演算法的plus版本。它的本質是多個分組同時進行插入排序演算法,所以會比插入排序演算法更加高效。核心是,這個多個分組是按照佇列的下標進行一定的增量分組。
其實現程式碼如下:
function shellSort(array) { var length = array.length; var gap = Math.round(length / 2); while(gap > 0) { for(var i = gap; i < length; i++) { var insert = array[i]; var index = i; for(var j = i; j >= 0; j-=gap) { if(insert < array[j]) { array[j+gap] = array[j]; index = j; } } array[index] = insert; } console.log(array); gap = Math.round(gap/2 - 0.1); } return array; } var arr = [11,8,5,6,3,10,7,8,2]; var result = shellSort(arr);
這個沒有流程圖,只能一步一步解釋了。首先,gap就是增量,通過整個流程最後可以得出,gap依次取值為:5,2,1,0;
一輪排序:根據增量5,對陣列[11,8,5,6,3,10,7,8,2]而言就是11和10比較,8和7比較,5和8比較,6和2比較,最後剩下一個3不動;兩兩比較,大小互換位置,得到陣列[10,7,5,2,3,11,8,8,6];
注意,這裡互換位置的時候是按照下標來互換的,這樣光說看起來比較蒼白無力,換成二維的吧
二輪排序:此時增量為2,對陣列[10,7,5,2,3,11,8,8,6]而言就是分為[10,5,3,8,6]比較,[7,2,11,8]比較,兩組分別進行插入演算法,大小互換位置,得到[3,5,6,8,10]和[2,7,8,11],即是[3,2,5,7,6,8,8,11,10];
三輪排序:此時增量為1,對陣列[3,2,5,7,6,8,8,11,10]直接進行插入排序演算法,就得到了[2,3,5,6,7,8,8,10,11]。
我去,突然想起來為啥不用excel來排版,算了,將就看吧。
6.歸併排序演算法
一種典型的分而治之思想的演算法應用,歸併排序演算法的實現有兩種方法:
- 自上而下的遞迴
- 自下而上的迭代
分治思想就是把一個大問題切分為若干個小問題,然後分別解決小問題,用這些小問題的答案來解釋大問題。拿到歸併演算法來說,我覺得歸併排序演算法先是不斷進行二分,然後最小單位的兩個進行比較,形成若干個排序序列,最後再和二分法反著來將之前分開的若干已排列好的佇列從最小單位開始慢慢比較頭部然後合併回去。至於迭代和遞迴,以前看到知乎上一大佬這樣形容,用電影來佐證,迭代是《明日邊緣》,遞迴是《盜夢空間》,令人拍案叫絕,至今印象深刻。
其實現程式碼如下(採用第一種自上而下的遞迴方式):
function mergeSort(arr) { var len = arr.length; if(len < 2) { return arr; } var middle = Math.floor(len / 2), left = arr.slice(0, middle), right = arr.slice(middle); return merge(mergeSort(left), mergeSort(right)); } function merge(left, right) { var result = []; while (left.length && right.length) { if (left[0] <= right[0]) { result.push(left.shift()); } else { result.push(right.shift()); } } while (left.length) result.push(left.shift()); while (right.length) result.push(right.shift());
console.log(result) return result; } var arr = [11,8,5,6,3,10,7,8,2]; var result = mergeSort(arr);
排序流程圖;
7.快速排序演算法
如果說希爾排序演算法是插入排序演算法的plus版本,那麼快速排序演算法算是氣泡排序的plus版本了吧。其通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序佇列。快速排序演算法,字如其演算法,聽說是排序演算法中數一數二的快了,效率非常高。上文也說到了,這是舊版的排序sort()在陣列長度大於等於10的情況下采用的排序演算法。
其實現程式碼如下:
function quickSort(array) { var length = array.length; if(length <= 1) { return array; }else{ var smaller = []; var bigger = []; var base = [array[0]]; for(var i = 1; i < length; i++) { if(array[i] <= base[0]) { smaller.push(array[i]); }else{ bigger.push(array[i]); } } console.log(smaller.concat(base.concat(bigger))); return quickSort(smaller).concat(base.concat(quickSort(bigger))); } } var arr = [11,8,5,6,3,10,7,8,2]; var result = quickSort(arr);
排序流程圖(這個流程圖是按照array[i] < base[0],主要是注意陣列[11,8,5,6,3,10,7,8,2]中兩個元素8的站位問題):
是不是還是看不懂?再上一個圖,這下看懂了吧,就是逼著你站隊,以每輪的第一個元素為基準,你比它大就站到右邊隊伍去,比它小就站到左邊隊伍去,直到最後每個隊伍都只剩自己孤單一元素的時候,排序就完成了。(這個圖是按照array[i] <= base[0]分析的,主要是注意陣列[11,8,5,6,3,10,7,8,2]中兩個元素8的站位問題)
8.其他的排序演算法
其實還有其他的排序演算法,像什麼堆排序、計數排序、基數排序、桶排序,這裡就不展開了,我也不會,手動狗頭。
亂序演算法
說了辣麼多排序演算法,再來點亂序演算法,先說點題外話,肯定很多人覺得利用sort方法就能實現亂序,其實現程式碼如下:
function randomSort() { return Math.random()-0.5; } var arr = [1,2,3,4,5,6,7,8,9]; var result = arr.sort(randomSort);
以前我也是這麼認為的,但是之後看到一篇文章說,Math.random()是個偽隨機,於是我去官網看了下api:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math/random
也就是說random其實是以初始種子作為基準,一般都是預設取時間戳為種子,然後進行一系列固定的演算法最終得到一個介於0和1之間的浮點數,也就是說只要知道種子的值,最後的結果是可以復刻的。最後我還是不死心,想自己測試下,於是就有了以下程式碼:
function test(num){ var arr = [0,1,2,3,4,5,6,7,8,9]; var total=[0,0,0,0,0,0,0,0,0,0]; var result=[]; for(var i=0;i<num;i++){ var tempArr=[...arr].sort(randomSort); for(var j=0;j<tempArr.length;j++){ total[j]+=tempArr[j]; } } total.forEach(item=>{ item=parseFloat((item/num).toFixed(3)); result.push(item) }) return result; } var num=1000; var result=test(num); console.log(result);
先解釋下,如果random是隨機的,那麼陣列[0,1,2,3,4,5,6,7,8,9]經過n次之後的randomSort方法,那麼最後每次每個位置上的值應該會無限趨同於平均值4.5,但是從上圖我們可以看出,明顯數值越大的出現的機率會更高,所以它確實是個偽隨機數。所以,真正的亂序演算法在下面。
1.Fisher–Yates(費舍爾耶茨)演算法,洗牌演算法
恩,這個名字就很直接了,和插入排序演算法可以搞一桌子了。洗牌的動作嘛我不說都很熟悉,就是拿出一坨牌隨機插入牌堆的位置,不斷重複幾次順序就亂了。
其實現程式碼如下:
function shuffle(array) { for(var i = array.length-1; i >=0; i--) { var randomIndex = Math.floor(Math.random()*(i+1)); var itemAtIndex = array[randomIndex]; array[randomIndex] = array[i]; array[i] = itemAtIndex; console.log(array); } return array; } var arr = [2,3,5,6,7,8,8,10,11]; var result = shuffle(arr);
最後把我寫的測試方法修改一下,將var tempArr=[...arr].sort(randomSort);替換為var tempArr=shuffle([...arr]);得出的結果如下,大部分都接近於4.5,甚至在1000000次的運算中,幾乎每個位置都等於4.5了。
2.隨機抽取演算法
這個就和洗牌演算法差不多了,異曲同工,看下程式碼就懂了。
其實現程式碼如下:
function randomlySelected(arr) { var array= []; while (arr.length) { var randomIndex = Math.floor(Math.random()*arr.length); array.push(arr[randomIndex]); arr.splice(randomIndex, 1);
console.log(array); } return array; } var arr = [2,3,5,6,7,8,8,10,11]; var result = randomlySelected(arr);
測試的結果也挺不錯。
不知不覺,就碼了這麼多字了,子曰:溫故而知新,總結這些js排序演算法和亂序演算法,不僅是對自己過往認知的總結,也是對之前的模糊不清的地方進行了梳理,感覺又深刻了億內內了呢,告辭。