現在前端對於算法的要求是越來越高了,以下簡單歸納下前端的幾種基本的排序算法與二分查找相關的內容
二分查找
二分查找也稱折半查找(Binary Search),它是一種效率較高的查找方法。但是,折半查找要求線性表必須采用順序存儲結構,而且表中元素按關鍵字有序排列。
在有序的數組中查詢一個元素用二分查找法是非常高效的,在應用中可以簡單的分為三種情況,即:查找目標值,查找比目標值大的第一個元素,查找比目標值小的第一個元素。
查找目標值
function binarySearch(arr, target) { let l = 0 let r = arr.length - 1 let mid = 0 while(l <= r) { mid = (l + r) >> 1 if (arr[mid] > target) { r = mid - 1 } else if (arr[mid] < target) { l = mid + 1 } else { return mid } } return -1 }
查找比目標值大的第一個元素
function binarySearchFirstGreate(arr, target) { let l = 0 let r = arr.length - 1 let mid = 0 while(l <= r) { mid = (l + r) >> 1 if (arr[mid] > target) { r = mid - 1 } else { l = mid + 1 } } return l }
查找比目標值小的第一個元素
function binarySearchFirstLess(arr, target) { let l = 0 let r = arr.length - 1 let mid = 0 while(l <= r) { mid = (l + r) >> 1 if (arr[mid] < target) { l = mid + 1 } else { r = mid - 1 } } return r }
選擇排序
選擇排序的工作原理是:第一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,然后再從剩余的未排序元素中尋找到最小(大)元素,然后放到已排序的序列的末尾。以此類推,直到全部待排序的數據元素的個數為零。
選擇排序是不穩定的排序方法。
function selectionSort(arr) { let l = arr.length for(let i = 0; i < l; i++) { for(let j = i + 1; j < l; j++) { if (arr[i] > arr[j]) { [arr[i], arr[j]] = [arr[j], arr[i]] } } } }
插入排序
插入排序,一般也被稱為直接插入排序。對於少量元素的排序,它是一個有效的算法。
插入排序是一種最簡單的排序方法,它的基本思想是將一個記錄插入到已經排好序的有序表中,從而一個新的、記錄數增1的有序表。在其實現過程使用雙層循環,外層循環對除了第一個元素之外的所有元素,內層循環對當前元素前面有序表進行待插入位置查找,並進行移動
它與選擇排序的區別是:
- 選擇排序是在未排列的數據中選取最大(小)的值。
- 插入排序是在已排列的數據中尋找正確的位置,所以插入排序比選擇排序性能會好很多。
function insertSort(arr) { let l = arr.length for(let i = 1; i < l; i++) { for(let j = i; j > 0; j--) { if (arr[j - 1] > arr[j]) { [arr[j - 1], arr[j]] = [arr[j], arr[j - 1]] } } } }
希爾排序(增強版的插入排序)
希爾排序(Shell's Sort)是插入排序的一種又稱“縮小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法。
希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至 1 時,整個文件恰被分成一組,算法便終止。
function shellSort(arr) { let t = new Date() let len = arr.length let h = 1 while(h < len / 3) h = 3 * h + 1 // 1, 4, 13, 40, 121, 364, 1093 while(h >= 1) { // 將數組變為h有序 for(let i = h; i < len; i++) { for(let j = i; j >= h; j -= h) { if (arr[j] < arr[j - h]) { [arr[j - h], arr[j]] = [arr[j], arr[j - h]] } } } h = Math.floor(h / 3) } }
歸並排序
歸並排序(Merge Sort)是建立在歸並操作上的一種有效,穩定的排序算法,該算法是采用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合並,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合並成一個有序表,稱為二路歸並。
首先歸並排序需要一個將兩個有序數組合並的方法:
function merge(a, l, m, r) { let i = l, j = m + 1, aux = [] for (let k = l;k <= r; k++) { aux[k] = a[k] } for (let k = l; k <= r; k++) { if (i > m) { a[k] = aux[j++] } else if (j > r) { a[k] = aux[i++] } else if (aux[j] < aux[i]) { a[k] = aux[j++] } else { a[k] = aux[i++] } } return a }
歸並排序的算法可以分為兩種方式:
-
自頂向下:采用遞歸的方式,不斷的將分割的子數組,直到將子數組的個數分割成1,然后再用merge合並成一個有序的大數組
-
自底向上:采用雙層循環的方式,先將數組內的元素與相鄰元素歸並,然后遞增到最后的一個大數組
自頂向下
function sort_down(a, l, r) { if (l >= r) return let m = (l + r) >> 1 sort_down(a, l, m) // 左邊排序 sort_down(a, m + 1, r) // 右邊排序 if (a[m] > a[m + 1]) { merge(a, l, m, r) // 合並 } }
自底向上
function sort_up(a) { let n = a.length for (let i = 1; i < n; i += i) { for (let j = 0; j < n - i; j += i + i) { merge(a, j, i + j - 1, Math.min(j + i + i - 1, n - 1)) } } }
快速排序
快速排序(Quicksort)是對冒泡排序算法的一種改進。
快速排序是通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然后再按此方法對這兩部分數據分別進行快速的原地排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
快速排序分為兩種方式:
- 二向切分快速排序:先進行左指針的值與base的比較,如果比base大,則從右指針遞減與base的比較,如果遇到比base小的則進行左指針與右指針互換,以此規則循環,直到比base小的都在左,大的都在右。然后進行遞歸直到整個數組有序。
- 三向切分快速排序:設立三個指針:左指針,中指針,右指針。三向切分只比較中指針指向的值與base的大小,如果中指針小於base,則左指針與中指針互換且都遞增1,如果比base大,則中指針與右指針互換,繼續與base比較,如果相等,則中指針加1,直到整個數組的左部分都比base小,右部分都比base大,然后遞歸直到整個數組有序。(左指針的索引是與base最近的,直到右指針的索引靠近base,則該循環結束。)
三向切分比二向切分的優化點在於:如果數組能有重復值的話,三向切分不需要重復比較,而二向切分是要重復比較的,對於大批量的用戶數據排序,該特性非常有用。
三向切分圖
二向切分快速排序
function quickSort(arr, l, r) { if (l >= r) return let base = arr[l] let i = l let j = r while(l <= r) { while(l < r && arr[++l] < base) {} while(l < r && arr[--r] > base) {} if (l < r) { [arr[l], arr[r]] = [arr[r], arr[l]] } else { [arr[l - 1], arr[i]] = [base, arr[l - 1]] break } } quicksort(arr, i, l - 2) quicksort(arr, l, j) }
三向切分快速排序
function sQuickSort(arr, l, r) { if (l >= r) return let lf = l let ri = r let v = arr[lf] let i = l + 1 while(i <= ri) { if (v > arr[i]) { [arr[lf++], arr[i++]] = [arr[i], arr[lf]] } else if (v < arr[i]) { [arr[i], arr[ri--]] = [arr[ri], arr[i]] } else { i++ } } squicksort(arr, l, lf - 1) squicksort(arr, ri + 1, r) }
堆排序
堆排序是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
堆有兩個重要的基本操作,即在堆有序時對單個元素的下沉和上浮操作。
以大頂堆為例(大頂堆即堆頂元素為最大,小頂堆的堆頂元素為最小):
大頂堆的下沉:
function sink(arr, k, len) { while(2 * k + 1 < len) { let j = 2 * k + 1 if (j < len - 1 && arr[j] < arr[j + 1]) j++ if (arr[k] >= arr[j]) break [arr[k], arr[j]] = [arr[j], arr[k]] k = j } }
大頂堆的上浮:
function swim(arr, len) { let p = 0 // 父級節點 while(len > 0) { p = (len - 1) >> 1 // (len & 1) 為0的情況下是有兄弟節點的,選出最大的與父級節點比較 if ((len & 1) === 0 && arr[len] < arr[len - 1]) len-- if (arr[len] <= arr[p]) break [arr[len], arr[p]] = [arr[p], arr[len]] len = p } }
在有序堆中每次添加和刪除元素后執行的下沉和上浮操作,都會得到目前有序堆中的最大(小)元素,以此特性就可以進行對元素排序。
function heapSort(arr) { let len = arr.length let copy = [] // 建立一個有序的堆 for (let i = (len - 1) >> 1; i >= 0; i--) { sink(arr, i, len) } // 每次將堆頂元素與堆尾元素進行替換,再進行堆頂元素的下沉且堆長度減一,以此可以得到一個有序的數據 while(len--) { [arr[0], arr[len]] = [arr[len], arr[0]] sink(arr, 0, len) } }
大頂堆的的排序得到的是一個降序排序,小頂堆的則得到的是升序數據。
簡單描述各排序算法的性能特點:
算法 | 是否穩定 | 是否原地排序 | 時間復雜度 | 空間復雜度 | 備注 |
選擇排序 | 否 | 是 | N2 | 1 | 取決於輸入元素的排列情況 |
插入排序 | 是 | 是 | 介於N和N2之間 | 1 | |
希爾排序 | 否 | 是 | NlogN? N6/5? |
1 | |
快速排序 | 否 | 是 | NlogN | lgN | 運行效率由概率提供保證 |
三向快速排序 | 否 | 是 | 介於N和NlogN之間 | lgN | 運行效率由概率保證,同時也 取決於輸入元素的分布情況 |
歸並排序 | 是 | 否 | NlogN |
N | |
堆排序 | 否 | 是 | NlogN | 1 |