作編程,排序是個必然的需求。前端也不例外,雖然很少,可是你確定會遇到。javascript
不過說到排序,最容易想到的就是冒泡排序,選擇排序,插入排序了。前端
依次比較相鄰的兩個元素,若是後一個小於前一個,則交換,這樣從頭至尾一次,就將最大的放到了末尾。java
從頭至尾再來一次,因爲每進行一輪,最後的都已是最大的了,所以後一輪須要比較次數能夠比上一次少一個。雖然你仍是可讓他從頭至尾來比較,可是後面的比較是沒有意義的無用功,爲了效率,你應該對代碼進行優化。node
圖片演示以下:git
代碼實現:算法
function bubbleSort(arr) { var len = arr.length; for (var i = 0; i < len - 1; i++) { for (var j = 0; j < len - 1 - i; j++) { if (arr[j] > arr[j+1]) { // 相鄰元素兩兩對比 var temp = arr[j+1]; // 元素交換 arr[j+1] = arr[j]; arr[j] = temp; } } } return arr; } |
選擇排序我以爲是最簡單的了,大一學VB的時候,就只記住了這個排序方法,原理很是簡單:每次都找一個最大或者最小的排在開始便可。chrome
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置編程
再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到已排序序列的末尾。數組
重複第二步,直到全部元素均排序完畢。數據結構
動圖演示:
代碼演示:
function selectionSort(arr) { var len = arr.length; var minIndex, temp; for (var i = 0; i < len - 1; i++) { minIndex = i; for (var j = i + 1; j < len; j++) { if (arr[j] < arr[minIndex]) { // 尋找最小的數 minIndex = j; // 將最小數的索引保存 } } temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; } return arr; } |
插入排序也比較簡單。就像打撲克同樣,依次將拿到的元素插入到正確的位置便可。
將第一待排序序列第一個元素看作一個有序序列,把第二個元素到最後一個元素當成是未排序序列。
從頭至尾依次掃描未排序序列,將掃描到的每一個元素插入有序序列的適當位置。(若是待插入的元素與有序序列中的某個元素相等,則將待插入元素插入到相等元素的後面。)
動圖演示:
代碼示例:
function insertionSort(arr) { var len = arr.length; var preIndex, current; for (var i = 1; i < len; i++) { preIndex = i - 1; current = arr[i]; while(preIndex >= 0 && arr[preIndex] > current) { arr[preIndex+1] = arr[preIndex]; preIndex--; } arr[preIndex+1] = current; } return arr; } |
上面三種都是很是簡單的排序方法,簡單的同時呢,效率也會比較低,仍是拿這本書裏的對比圖來講明:
時間複雜度都高達 O(n^2) ,而它們後面的一些排序算法時間複雜度基本都只有 O(n log n) 。
個人強迫症又犯了,我想要高效率一點的排序方法。
簡單把這本書的內容過了一遍,當時就理解了這個歸併排序,所以這裏就談一下這個歸併排序吧。
基本原理是分治法,就是分開而且遞歸來排序。
步驟以下:
動圖演示:
代碼示例:
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()); return result; } |
既然是個愛折騰的人,折騰了總得看看效果吧。
因爲我學這個來進行排序不是對簡單數組,數組內都是對象,要對對象的某個屬性進行排序,還要考慮升降序。
所以個人代碼實現以下:
/** * [歸併排序] * @param {[Array]} arr [要排序的數組] * @param {[String]} prop [排序字段,用於數組成員是對象時,按照其某個屬性進行排序,簡單數組直接排序忽略此參數] * @param {[String]} order [排序方式 省略或asc爲升序 不然降序] * @return {[Array]} [排序後數組,新數組,並不是在原數組上的修改] */ var mergeSort = (function() { // 合併 var _merge = function(left, right, prop) { var result = []; // 對數組內成員的某個屬性排序 if (prop) { while (left.length && right.length) { if (left[0][prop] <= right[0][prop]) { result.push(left.shift()); } else { result.push(right.shift()); } } } else { // 數組成員直接排序 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()); return result; }; var _mergeSort = function(arr, prop) { // 採用自上而下的遞歸方法 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, prop), _mergeSort(right, prop), prop); }; return function(arr, prop, order) { var result = _mergeSort(arr, prop); if (!order || order.toLowerCase() === 'asc') { // 升序 return result; } else { // 降序 var _ = []; result.forEach(function(item) { _.unshift(item); }); return _; } }; })(); |
須要對哪一個屬性進行排序是不肯定,能夠隨意指定,所以寫成了參數。有因爲不想讓這些東西在每次循環都進行判斷,所以代碼有點冗餘。
關於降序的問題,也沒有加入參數中,而是簡單的升序後再逆序輸出。緣由是不想讓每次循環遞歸裏都去判斷條件,因此簡單處理了。
下面就是見證效率的時候了,一段數據模擬:
var getData = function() { return Mock.mock({ "list|1000": [{ name: '@cname', age: '@integer(0,500)' }] }).list; }; |
實際測試來啦:
// 效率測試 var arr = getData(); console.time('歸併排序'); mergeSort(arr, 'age'); console.timeEnd('歸併排序'); console.time('冒泡排序'); for (var i = 0, l = arr.length; i < l - 1; ++i) { var temp; for (var j = 0; j < l - i - 1; ++j) { if (arr[j].age > arr[j + 1].age) { temp = arr[j + 1]; arr[j + 1] = arr[j]; arr[j] = temp; } } } console.timeEnd('冒泡排序'); |
進行了五次,效果以下:
// 歸併排序: 6.592ms // 冒泡排序: 25.959ms // 歸併排序: 1.334ms // 冒泡排序: 20.078ms // 歸併排序: 1.085ms // 冒泡排序: 16.420ms // 歸併排序: 1.200ms // 冒泡排序: 16.574ms // 歸併排序: 2.593ms // 冒泡排序: 12.653ms |
最低4倍,最高近16倍的效率之差仍是比較滿意的。
雖然 1000 條數據讓前端排序的可能性不大,可是幾十上百條的狀況仍是有的。另外因爲node, JavaScript 也能運行的服務端了,這個效率的提高也仍是有用武之地的。
歸併排序裏面使用了遞歸,在《數據結構與算法 JavaScript 描述》中,做者給出了自下而上的迭代方法。可是對於遞歸法,做者卻認爲:
However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
然而,在 JavaScript 中這種方式不太可行,由於這個算法的遞歸深度對它來說太深了。
gitbook上這本書的做者對此有疑問,我也有疑問。
歸併中雖然用了遞歸,可是他是放在 return 後的呀。關於在renturn後的遞歸是有尾遞歸優化的呀。
關於尾遞歸優化是指:原本外層函數內部再調用一個函數的話,因爲外層函數須要等待內層函數返回後才能返回結果,進入內層函數後,外層函數的信息,內存中是必須記住的,也就是調用堆棧。而內部函數放在 return 關鍵字後,就表示外層函數到此也就結束了,進入內層函數後,沒有必要再記住外層函數內的全部信息。
上面是個人理解的描述,不知道算不算準確。 chrome 下已經能夠開啓尾遞歸優化的功能了,我以爲這個遞歸是不應影響他在 JavaScript 下的使用的。
有興趣的話,推薦讀讀這本書,進行排序的時候,能夠考慮一些更高效的方法。
不過須要注意的是,這些高效率的排序方法,通常都須要相對較多的額外內存空間,須要權衡一下。
另外,很是小規模的數據就沒有必要了。一是影響過小,而是咱們人的效率問題,一分鐘能從頭寫個冒泡、選擇、插入的排序方法,而換成是歸併排序呢?
來自:http://blog.cdswyda.com/post/javascript/2017-03-22-js-sort-not-only-bubblesort