JavaScript 排序,不只是冒泡
做程式設計,排序是個必然的需求。前端也不例外,雖然不多,但是你肯定會遇到。
不過說到排序,最容易想到的就是氣泡排序,選擇排序,插入排序了。
氣泡排序
依次比較相鄰的兩個元素,如果後一個小於前一個,則交換,這樣從頭到尾一次,就將最大的放到了末尾。
從頭到尾再來一次,由於每進行一輪,最後的都已經是最大的了,因此後一輪需要比較次數可以比上一次少一個。雖然你還是可以讓他從頭到尾來比較,但是後面的比較是沒有意義的無用功,為了效率,你應該對程式碼進行優化。
圖片演示如下:
程式碼實現:
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的時候,就只記住了這個排序方法,原理非常簡單:每次都找一個最大或者最小的排在開始即可。
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。
- 重複第二步,直到所有元素均排序完畢。
動圖演示:
程式碼演示:
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)
。
我的強迫症又犯了,我想要高效率一點的排序方法。
歸併排序
簡單把這本書的內容過了一遍,當時就理解了這個歸併排序,因此這裡就談一下這個歸併排序吧。
基本原理是分治法,就是分開並且遞迴來排序。
步驟如下:
- 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列;
- 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置;
- 比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置;
- 重複步驟 3 直到某一指標達到序列尾;
- 將另一序列剩下的所有元素直接複製到合併序列尾。
動圖演示:
程式碼示例:
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; };
上面使用Mock
進行了模擬資料,關於Mock : http://mockjs.com/
實際測試來啦:
// 效率測試 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
下的使用的。
最後
有興趣的話,推薦讀讀這本書,進行排序的時候,可以考慮一些更高效的方法。
不過需要注意的是,這些高效率的排序方法,一般都需要相對較多的額外記憶體空間,需要權衡一下。
另外,非常小規模的資料就沒有必要了。一是影響太小,而是我們人的效率問題,一分鐘能從頭寫個冒泡、選擇、插入的排序方法,而換成是歸併排序呢?
相關文章
- JavaScript氣泡排序+Vue視覺化冒泡動畫JavaScript排序Vue視覺化動畫
- 冒泡和快速排序排序
- JavaScript 事件冒泡JavaScript事件
- JavaScript阻止事件冒泡JavaScript事件
- 冒泡,選擇排序法(C#)排序C#
- 7-7 冒泡法排序 (20分)排序
- 選擇法和冒泡法排序介面排序
- 七、排序,選擇、冒泡、希爾、歸併、快速排序實現排序
- javascript阻止事件冒泡程式碼JavaScript事件
- C語言排序 冒泡 選擇 快排C語言排序
- 排序法:選擇、冒泡、插入和快排排序
- javascript事件冒泡簡單例項JavaScript事件單例
- 單連結串列的排序(插入,選擇,冒泡)排序
- 插入、冒泡、歸併、堆排序、快排總結排序
- python排序演算法的實現-冒泡Python排序演算法
- JavaScript 阻止事件冒泡程式碼例項JavaScript事件
- Javascript中的事件冒泡與捕獲JavaScript事件
- JavaScript事件捕獲冒泡與捕獲JavaScript事件
- javascript基礎(事件的冒泡)(三十二)JavaScript事件
- 排序演算法Python(冒泡、選擇、快速、插入、希爾、歸併排序)排序演算法Python
- js實現兩種實用的排序演算法——冒泡、快速排序JS排序演算法
- 基礎排序(冒泡、選擇、插入)學習筆記排序筆記
- 排序演算法之冒泡,選擇,插入和希爾排序演算法
- PHP排序演算法(插入,選擇,交換,冒泡,快速)PHP排序演算法
- javascript事件冒泡和事件捕獲型別JavaScript事件型別
- 指標對陣列排序選擇法和冒泡法指標陣列排序
- 常見的排序演算法:冒泡、快排、歸併排序演算法
- 快速排序javaScript排序JavaScript
- javascript 快速排序JavaScript排序
- JavaScript和JQuery的滑鼠mouse事件冒泡處理JavaScriptjQuery事件
- 原生javascript的return false並不能阻止事件冒泡JavaScriptFalse事件
- JavaScript阻止冒泡和取消事件預設行為JavaScript事件
- javascript的事件監聽與捕獲和冒泡JavaScript事件
- 反射,hashlib模組,正則匹配,冒泡,選擇,插入排序反射排序
- 陣列基本操作及冒泡演算法、直接選擇排序陣列演算法排序
- 前端學習程式碼例項-JavaScript阻止事件冒泡前端JavaScript事件
- JavaScript事件冒泡、事件捕獲和阻止預設事件JavaScript事件
- JavaScript氣泡排序JavaScript排序