前言
排序演算法可能是你學程式設計第一個學習的演算法,還記得冒泡嗎?
當然,排序和查詢兩類演算法是面試的熱門選項。如果你是一個會寫快排的程式猿,面試官在比較你和一個連快排都不會寫的人的時候,會優先選擇你的。那麼,前端需要會排序嗎?答案是毋庸置疑的,必須會。現在的前端對計算機基礎要求越來越高了,如果連排序這些演算法都不會,那麼發展前景就有限了。本篇將會總結一下,在前端的一些排序演算法。如果你喜歡我的文章,歡迎評論,歡迎Star~。歡迎關注我的github部落格
正文
首先,我們可以先來看一下js自身的排序演算法sort()
Array.sort
相信每個使用js的都用過這個函式,但是,這個函式本身有些優點和缺點。我們可以通過一個例子來看一下它的功能:
const arr = [1, 20, 10, 30, 22, 11, 55, 24, 31, 88, 12, 100, 50];
console.log(arr.sort()); //[ 1, 10, 100, 11, 12, 20, 22, 24, 30, 31, 50, 55, 88 ]
console.log(arr.sort((item1, item2) => item1 - item2)); //[ 1, 10, 11, 12, 20, 22, 24, 30, 31, 50, 55, 88, 100 ]複製程式碼
相信你也已經看出來它在處理上的一些差異了吧。首先,js中的sort會將排序的元素型別轉化成字串進行排序。不過它是一個高階函式,可以接受一個函式作為引數。而我們可以通過傳入內部的函式,來調整陣列的升序或者降序。
sort函式的效能:相信對於排序演算法效能來說,時間複雜度是至關重要的一個參考因素。那麼,sort函式的演算法效能如何呢?通過v8引擎的原始碼可以看出,Array.sort是通過javascript來實現的,而使用的演算法是快速排序,但是從原始碼的角度來看,在實現上明顯比我們所使用的快速排序複雜多了,主要是做了效能上的優化。所以,我們可以放心的使用sort()進行排序。
氣泡排序
氣泡排序,它的名字由來於一副圖——魚吐泡泡,泡泡越往上越大。
回憶起這個演算法,還是最初大一的c++課上面。還是自己上臺,在黑板上實現的呢!
思路:第一次迴圈,開始比較當前元素與下一個元素的大小,如果比下一個元素小或者相等,則不需要交換兩個元素的值;若比下一個元素大的話,則交換兩個元素的值。然後,遍歷整個陣列,第一次遍歷完之後,相同操作遍歷第二遍。
圖例:
程式碼實現:
const arr = [1, 20, 10, 30, 22, 11, 55, 24, 31, 88, 12, 100, 50];
function bubbleSort(arr){
for(let i = 0; i < arr.length - 1; i++){
for(let j = 0; j < arr.length - i - 1; j++){
if(arr[j] > arr[j + 1]){
swap(arr, j, j+1);
}
}
}
return arr;
}
function swap(arr, i, j){
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
console.log(arr);複製程式碼
效能:
- 時間複雜度:平均時間複雜度是O(n^2)
- 空間複雜度:由於輔助空間為常數,所以空間複雜度是O(1);
改進:
我們可以對氣泡排序進行改進,使得它的時間複雜度在大多數順序的情況下,減小到O(n);
- 加一個標誌位,如果沒有進行交換,將標誌位置為false,表示排序完成。
const arr = [1, 20, 10, 30, 22, 11, 55, 24, 31, 88, 12, 100, 50];
function swap(arr, i, j){
const temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
for(let i = 0; i < arr.length - 1; i++){
let flag = false;
for(let j = 0; j < arr.length - 1 - i; j++){
if(arr[j] > arr[j+1]){
swap(arr, j, j+1);
flag = true;
}
}
if(!flag){
break;
}
}
console.log(arr); //[ 1, 10, 11, 12, 20, 22, 24, 30, 31, 50, 55, 88, 100 ]複製程式碼
- 記錄最後一次交換的位置, 因為最後一次交換的數,是在這一次排序當中最大的數,之後的數都比它大。在最佳狀態時,時間複雜度也會縮小到O(n);
const arr = [1, 20, 10, 30, 22, 11, 55, 24, 31, 88, 12, 100, 50 ,112];
function swap(arr, i, j){
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp
}
function improveBubble(arr, len){
for(let i = len - 1; i >= 0; i--){
let pos = 0;
for(let j = 0; j < i; j++){
if(arr[j] > arr[j+1]){
swap(arr, j, j+1);
pos = j + 1;
}
}
len = pos + 1;
}
return arr;
}
console.log(improveBubble(arr, arr.length)); //[ 1, 10, 11, 12, 20, 22, 24, 30, 31, 50, 55, 88, 100, 112 ]複製程式碼
選擇排序
選擇排序,即每次都選擇最小的,然後換位置
思路:
第一遍,從陣列中選出最小的,與第一個元素進行交換;第二遍,從第二個元素開始,找出最小的,與第二個元素進行交換;依次迴圈,完成排序
圖例:
程式碼實現:
const arr = [1, 20, 10, 30, 22, 11, 55, 24, 31, 88, 12, 100, 50];
function swap(arr, i, j){
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
function selectionSort(arr){
for(let i = 0; i < arr.length - 1; i++){
let index = i;
for(let j = i+1; j < arr.length; j++){
if(arr[index] > arr[j]){
index = j;
}
}
swap(arr, i, index);
}
return arr;
}
console.log(selectionSort(arr)); //[ 1, 10, 11, 12, 20, 22, 24, 30, 31, 50, 55, 88, 100 ]複製程式碼
效能:
時間複雜度:平均時間複雜度是O(n^2),這是一個不穩定的演算法,因為每次交換之後,它都改變了後續陣列的順序。
空間複雜度:輔助空間是常數,空間複雜度為O(1);
插入排序
插入排序,即將元素插入到已排序好的陣列中
思路:
首先,迴圈原陣列,然後,將當前位置的元素,插入到之前已排序好的陣列中,依次操作。
圖例:
程式碼實現:
const arr = [1, 20, 10, 30, 22, 11, 55, 24, 0, 31, 88, 12, 100, 50 ,112];
function insertSort(arr){
for(let i = 0; i < arr.length; i++){
let temp = arr[i];
for(let j = 0; j < i; j++){
if(temp < arr[j] && j === 0){
arr.splice(i, 1);
arr.unshift(temp);
break;
}else if(temp > arr[j] && temp < arr[j+1] && j < i - 1){
arr.splice(i, 1);
arr.splice(j+1, 0, temp);
break;
}
}
}
return arr;
}
console.log(insertSort(arr)); //[ 0, 1, 10, 11, 12, 20, 22, 24, 30, 31, 50, 55, 88, 100, 112 ]複製程式碼
效能:
- 時間複雜度:平均演算法複雜度為O(n^2)
- 空間複雜度:輔助空間為常數,空間複雜度是O(1)
我們仨之間
其實,三個演算法都是難兄難弟,因為演算法的時間複雜度都是在O(n^2)。在最壞情況下,它們都需要對整個陣列進行重新調整。只是選擇排序比較不穩定。
快速排序
快速排序,從它的名字就應該知道它很快,時間複雜度很低,效能很好。它將排序演算法的時間複雜度降低到O(nlogn)
思路:
首先,我們需要找到一個基數,然後將比基數小的值放在基數的左邊,將比基數大的值放在基數的右邊,之後進行遞迴那兩組已經歸類好的陣列。
圖例:
原圖片太大,放一張小圖,並且附上原圖片地址,有興趣的可以看一下:
程式碼實現:
const arr = [30, 32, 6, 24, 37, 32, 45, 21, 38, 23, 47];
function quickSort(arr){
if(arr.length <= 1){
return arr;
}
let temp = arr[0];
const left = [];
const right = [];
for(var i = 1; i < arr.length; i++){
if(arr[i] > temp){
right.push(arr[i]);
}else{
left.push(arr[i]);
}
}
return quickSort(left).concat([temp], quickSort(right));
}
console.log(quickSort(arr));複製程式碼
效能:
- 時間複雜度:平均時間複雜度O(nlogn),只有在特殊情況下會是O(n^2),不過這種情況非常少
- 空間複雜度:輔助空間是logn,所以空間複雜度為O(logn)
歸併排序
歸併排序,即將陣列分成不同部分,然後注意排序之後,進行合併
思路:
首先,將相鄰的兩個數進行排序,形成n/2對,然後在每兩對進行合併,不斷重複,直至排序完。
圖例:
程式碼實現:
//迭代版本
const arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]
function mergeSort(arr){
const len = arr.length;
for(let seg = 1; seg < len; seg += seg){
let arrB = [];
for(let start = 0; start < len; start += 2*seg){
let row = start, mid = Math.min(start+seg, len), heig = Math.min(start + 2*seg, len);
let start1 = start, end1 = mid;
let start2 = mid, end2 = heig;
while(start1 < end1 && start2 < end2){
arr[start1] < arr[start2] ? arrB.push(arr[start1++]) : arrB.push(arr[start2++]);
}
while(start1 < end1){
arrB.push(arr[start1++]);
}
while(start2 < end2){
arrB.push(arr[start2++]);
}
}
arr = arrB;
}
return arr;
}
console.log(mergeSort(arr));複製程式碼
//遞迴版
const arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
function mergeSort(arr, seg = 1){
const len = arr.length;
if(seg > len){
return arr;
}
const arrB = [];
for(var start = 0; start < len; start += 2*seg){
let low = start, mid = Math.min(start+seg, len), heig = Math.min(start+2*seg, len);
let start1 = low, end1 = mid;
let start2 = mid, end2 = heig;
while(start1 < end1 && start2 < end2){
arr[start1] < arr[start2] ? arrB.push(arr[start1++]) : arrB.push(arr[start2++]);
}
while(start1 < end1){
arrB.push(arr[start1++]);
}
while(start2 < end2){
arrB.push(arr[start2++]);
}
}
return mergeSort(arrB, seg * 2);
}
console.log(mergeSort(arr));複製程式碼
效能:
- 時間複雜度:平均時間複雜度是O(nlogn)
- 空間複雜度:輔助空間為n,空間複雜度為O(n)
基數排序
基數排序,就是將數的每一位進行一次排序,最終返回一個正常順序的陣列。
思路:
首先,比較個位的數字大小,將陣列的順序變成按個位依次遞增的,之後再比較十位,再比較百位的,直至最後一位。
圖例:
程式碼實現:
const arr = [3221, 1, 10, 9680, 577, 9420, 7, 5622, 4793, 2030, 3138, 82, 2599, 743, 4127, 10000];
function radixSort(arr){
let maxNum = Math.max(...arr);
let dis = 0;
const len = arr.length;
const count = new Array(10);
const tmp = new Array(len);
while(maxNum >=1){
maxNum /= 10;
dis++;
}
for(let i = 1, radix = 1; i <= dis; i++){
for(let j = 0; j < 10; j++){
count[j] = 0;
}
for(let j = 0; j < len; j++){
let k = parseInt(arr[j] / radix) % 10;
count[k]++;
}
for(let j = 1; j < 10; j++){
count[j] += count[j - 1];
}
for(let j = len - 1; j >= 0 ; j--){
let k = parseInt(arr[j] / radix) % 10;
tmp[count[k] - 1] = arr[j];
count[k]--;
}
for(let j = 0; j < len; j++){
arr[j] = tmp[j];
}
radix *= 10;
}
return arr;
}
console.log(radixSort(arr));複製程式碼
效能:
- 時間複雜度:平均時間複雜度O(k*n),最壞的情況是O(n^2)
總結
我們一共實現了6種排序演算法,對於前端開發來說,熟悉前面4種是必須的。特別是快排,基本面試必考題。本篇的內容總結分為六部分:
- 氣泡排序
- 選擇排序
- 插入排序
- 快速排序
- 歸併排序
- 基數排序
排序演算法,是演算法的基礎部分,需要明白它的原理,總結下來排序可以分為比較排序和統計排序兩種方式,本篇前5種均為比較排序,基數排序屬於統計排序的一種。希望看完的你,能夠去動手敲敲程式碼,理解一下
如果你對我寫的有疑問,可以評論,如我寫的有錯誤,歡迎指正。你喜歡我的部落格,請給我關注Star~呦。大家一起總結一起進步。歡迎關注我的github部落格