面試官問:你會三路快排嗎?git
我:github
...面試
關於時間複雜度:算法
原地排序:特指空間複雜度是 O(1) 的排序算法。shell
穩定性:若是待排序的序列中存在值相等的元素,通過排序以後,相等元素之間原有的前後順序不變。api
冒泡排序(英語:Bubble Sort)又稱爲泡式排序,是一種簡單的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,若是他們的順序錯誤就把他們交換過來。走訪數列的工做是重複地進行直到沒有再須要交換,也就是說該數列已經排序完成。這個算法的名字由來是由於越小的元素會經由交換慢慢「浮」到數列的頂端。數組
冒泡排序對 n 個項目須要 O(n**2) 的比較次數,且能夠原地排序。儘管這個算法是最簡單瞭解和實現的排序算法之一,但它對於包含大量的元素的數列排序是很沒有效率的。微信
冒泡排序是與插入排序擁有相等的運行時間,可是兩種算法在須要的交換次數卻很大地不一樣。在最壞的狀況,冒泡排序須要 O(n**2) 次交換,而插入排序只要最多 O(n) 交換。冒泡排序的實現(相似下面)一般會對已經排序好的數列拙劣地運行(O(n ** 2)),而插入排序在這個例子只須要 O(n) 個運算。所以不少現代的算法教科書避免使用冒泡排序,而用插入排序取代之。冒泡排序若是能在內部循環第一次運行時,使用一個旗標來表示有無須要交換的可能,也能夠把最優狀況下的複雜度下降到 O(n) 。在這個狀況,已經排序好的數列就無交換的須要。若在每次走訪數列時,把走訪順序反過來,也能夠稍微地改進效率。有時候稱爲雞尾酒排序,由於算法會從數列的一端到另外一端之間穿梭往返。markdown
冒泡排序算法的運做以下:數據結構
export function bubbleSort(arr: number[]) {
const length = arr.length
if (length <= 1) return arr
for (let i = 0; i < length; i++) {
let changed: boolean = false // 沒有數據交換則表示已經有序了
for (let j = 0; j < length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1)
changed = true
}
}
if (!changed) break
}
return arr
}
複製代碼
雞尾酒排序
export function cocktailSort(arr: number[]) {
const len = arr.length
for (let i = 0; i < len / 2; i++) {
let start: number = 0
let end: number = len - 1
for (let j = start; j < end; j++) {
if (arr[j] > arr[j + 1]) swap(arr, j, j + 1)
}
end--
for (let j = end; j > start; j--) {
if (arr[j] < arr[j - 1]) swap(arr, j - 1, j)
}
start++
}
return arr
}
複製代碼
冒泡排序
雞尾酒排序
選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工做原理以下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,而後,再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到已排序序列的末尾。以此類推,直到全部元素均排序完畢。
選擇排序的主要優勢與數據移動有關。若是某個元素位於正確的最終位置上,則它不會被移動。選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,所以對 n 個元素的表進行排序總共進行至多 n - 1 次交換。在全部的徹底依靠交換去移動元素的排序方法中,選擇排序屬於很是好的一種。
export function selectionSort(arr: number[]) {
const length = arr.length
if (length <= 1) return arr
for (let i = 0; i < length; i++) {
let min = i
for (let j = i + 1; j < length; j++) {
if (arr[j] < arr[min]) {
min = j
}
}
swap(arr, i, min)
}
return arr
}
複製代碼
插入排序(英語:Insertion Sort)是一種簡單直觀的排序算法。它的工做原理是經過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,一般採用 in-place 排序(即只需用到 O(1) 的額外空間的排序),於是在從後向前掃描過程當中,須要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。
Insertion Sort 和打撲克牌時,從牌桌上逐一拿起撲克牌,在手上排序的過程相同。
通常來講,插入排序都採用 in-place 在數組上實現。具體算法描述以下:
export function insertionSort(arr: number[]) {
const length = arr.length
if (length <= 1) return arr
for (let i = 1; i < length; i++) {
const cur = arr[i]
let j = i - 1
for (; j >= 0; j--) {
if (arr[j] > cur) {
arr[j + 1] = arr[j]
} else {
break
}
}
arr[j + 1] = cur
}
return arr
}
複製代碼
or
export function insertionSort2(arr: number[]) {
const len = arr.length
for (let i = 1; i < len; i++) {
for (let j = i - 1; j >= 0; j--) {
if (arr[j] > arr[j + 1]) {
// 這裏是更改兩個元素,因此比上面的方法效率低
swap(arr, j + 1, j)
} else {
break
}
}
}
return arr
}
複製代碼
快速排序,快速排序(英語:Quicksort),又稱劃分交換排序(partition-exchange sort),簡稱快排,一種排序算法,最先由東尼·霍爾提出。在平均情況下,排序 n 個項目要 O(nlogn) (大 O 符號)次比較。在最壞情況下則須要 O(n**2) 次比較,但這種情況並不常見。事實上,快速排序 O(nlogn) 一般明顯比其餘算法更快,由於它的內部循環(inner loop)能夠在大部分的架構上頗有效率地達成。
快速排序使用 分治法(Divide and conquer) 策略來把一個序列(list)分爲較小和較大的 2 個子序列,而後遞歸地排序兩個子序列。
步驟爲:
遞歸到最底部的判斷條件是數列的大小是零或一,此時該數列顯然已經有序。
選取基準值有數種具體方法,此選取方法對排序的時間性能有決定性影響。
1 普通快排
function partition(arr: number[], left: number, right: number): number {
let pivot: number = left // 默認從最左邊開始,有優化空間
let index = pivot + 1
for (let i = index; i <= right; i++) {
if (arr[i] < arr[pivot]) {
swap(arr, i, index)
index++
}
}
swap(arr, pivot, index - 1)
return index - 1
}
export function quickSort(arr: number[], l?: number, r?: number) {
const len = arr.length
const left: number = typeof l === 'number' ? l : 0
const right: number = typeof r === 'number' ? r : len - 1
let partitionIndex = 0
if (left < right) {
partitionIndex = partition(arr, left, right)
quickSort(arr, left, partitionIndex - 1)
quickSort(arr, partitionIndex + 1, right)
}
return arr
}
複製代碼
2 左右指針快排
function partition(arr: number[], left: number, right: number): number {
let l: number = left // 默認從最左邊開始,有優化空間
let r: number = right
const target: number = arr[left]
while (l < r) {
while (arr[r] >= target && r > l) {
r--
}
while (arr[l] <= target && l < r) {
l++
}
swap(arr, l, r)
}
if (l !== left) {
swap(arr, l, left)
}
return l
}
export function quickSort2(arr: at, l?: number, r?: number) {
const len = arr.length
const left: number = typeof l === 'number' ? l : 0
const right: number = typeof r === 'number' ? r : len - 1
let partitionIndex = 0
if (left < right) {
partitionIndex = partition(arr, left, right)
quickSort2(arr, left, partitionIndex - 1)
quickSort2(arr, partitionIndex + 1, right)
}
return arr
}
複製代碼
3 三路快排
function partion(arr: at, l: number, r: number) {
// 基準數選取區間的第一個值
let v = arr[l]
let lt = l
let gt = r + 1
// 下面的循環很差理解
// i 和 gt 都在變化,gt 向左移動能夠不影響 i,lt 增加會把等於v的項轉移到 i,因此須要 i++
for (let i = l + 1; i < gt; ) {
if (arr[i] === v) {
// lt 和 i 在這裏拉開差距
i++
} else if (arr[i] > v) {
swap(arr, gt - 1, i)
gt--
} else {
swap(arr, lt + 1, i)
lt++
i++
}
}
swap(arr, l, lt) // arr[lt] === v
lt--
return { lt, gt }
}
export function quickSort3(arr: at, l?: number, r?: number) {
const len = arr.length
const left: number = typeof l === 'number' ? l : 0
const right: number = typeof r === 'number' ? r : len - 1
if (left >= right) return
let { lt, gt } = partion(arr, left, right)
quickSort3(arr, l, lt)
quickSort3(arr, gt, r)
return arr
}
複製代碼
希爾排序,也稱遞減增量排序算法,是插入排序的一種更高效的改進版本。希爾排序是非穩定排序算法。
希爾排序是基於插入排序的如下兩點性質而提出改進方法的:
希爾排序經過將比較的所有元素分爲幾個區域來提高插入排序的性能。這樣可讓一個元素能夠一次性地朝最終位置前進一大步。而後算法再取愈來愈小的步長進行排序,算法的最後一步就是普通的插入排序,可是到了這步,需排序的數據幾乎是已排好的了(此時插入排序較快)。
export function shellSort(arr: number[]) {
const length: number = arr.length
let i, j
// 調整 gap
for (let gap = length >> 1; gap > 0; gap >>= 1) {
// 按區間插排
for (i = gap; i < length; i++) {
let temp: number = arr[i]
// 從當前位置往左按區間掃描
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
arr[j + gap] = arr[j]
}
arr[j + gap] = temp
}
}
return arr
}
複製代碼
or
export function shellSort2(arr: number[]) {
const length: number = arr.length
let i, j
// 調整 gap
for (let gap = length >> 1; gap > 0; gap >>= 1) {
// 按區間插排
for (i = gap; i < length; i++) {
// 從當前位置往左按區間掃描
for (j = i - gap; j >= 0 && arr[j] > arr[j + gap]; j -= gap) {
// 這裏是更改兩個元素,因此比上面的方法效率低
swap(arr, j, j + gap)
}
}
}
return arr
}
複製代碼
歸併排序(英語:Merge sort,或 mergesort),是建立在歸併操做上的一種有效的排序算法,效率爲 O(nlogn)。1945 年由約翰·馮·諾伊曼首次提出。該算法是採用 分治法(Divide and Conquer) 的一個很是典型的應用,且各層分治遞歸能夠同時進行。
採用分治法:
歸併操做(merge),也叫歸併算法,指的是將兩個已經排序的序列合併成一個序列的操做。歸併排序算法依賴歸併操做。
歸併排序有兩種思路:
遞歸法(Top-down)
function merge(lArr: number[], rArr: number[]) {
const result: number[] = []
while (lArr.length && rArr.length) {
if (lArr[0] < rArr[0]) {
result.push(<number>lArr.shift())
} else {
result.push(<number>rArr.shift())
}
}
while (lArr.length) {
result.push(<number>lArr.shift())
}
while (rArr.length) {
result.push(<number>rArr.shift())
}
return result
}
function merge2(lArr: number[], rArr: number[]) {
const result: number[] = []
let lLen = lArr.length
let rLen = rArr.length
let i = 0
let j = 0
while (i < lLen && j < rLen) {
if (lArr[i] < rArr[j]) result.push(lArr[i++])
else result.push(rArr[j++])
}
while (i < lLen) result.push(lArr[i++])
while (j < rLen) result.push(rArr[j++])
return result
}
複製代碼
迭代法(Bottom-up)
原理以下(假設序列共有 n 個元素):
export function mergeSort2(arr: number[]): number[] {
const len = arr.length
for (let sz = 1; sz < len; sz *= 2) {
for (let i = 0; i < len - sz; i += 2 * sz) {
const start = i
const mid = i + sz - 1
const end = Math.min(i + 2 * sz - 1, len - 1)
merge(arr, start, mid, end)
}
}
return arr
}
function merge(arr: number[], start: number, mid: number, end: number) {
let i = start
let j = mid + 1
const tmp = []
let k = start
for (let w = start; w <= end; w++) {
tmp[w] = arr[w]
}
while (i < mid + 1 && j < end + 1) {
if (tmp[i] < tmp[j]) arr[k++] = tmp[i++]
else arr[k++] = tmp[j++]
}
while (i < mid + 1) arr[k++] = tmp[i++]
while (j < end + 1) arr[k++] = tmp[j++]
}
複製代碼
一般堆是經過一維數組來實現的。在數組起始位置爲 0 的情形中:
堆的操做
在堆的數據結構中,堆中的最大值老是位於根節點(在優先隊列中使用堆的話堆中的最小值位於根節點)。堆中定義如下幾種操做:
function heapifyMax(arr: at, i: number, len: number) {
const left = 2 * i + 1
const right = 2 * i + 2
let max = i
if (left < len && arr[left] > arr[max]) {
max = left
}
if (right < len && arr[right] > arr[max]) {
max = right
}
if (max !== i) {
swap(arr, max, i)
heapifyMax(arr, max, len)
}
}
function heapifyMin(arr: at, i: number, len: number) {
const left = 2 * i + 1
const right = 2 * i + 2
let min = i
if (left < len && arr[left] < arr[min]) {
min = left
}
if (right < len && arr[right] < arr[min]) {
min = right
}
if (min !== i) {
swap(arr, min, i)
heapifyMin(arr, min, len)
}
}
// 構建大頂堆
function buildMaxHeap(arr: at) {
const len = arr.length
for (let i = Math.floor(len / 2); i >= 0; i--) {
heapifyMax(arr, i, len)
}
}
// 構建小頂堆
function buildMinHeap(arr: at) {
const len = arr.length
for (let i = Math.floor(len / 2); i >= 0; i--) {
heapifyMin(arr, i, len)
}
}
// asc 爲 true 表示從小到大,false 爲從大到小
export function heapSort(arr: at, asc: boolean = true) {
if (asc) {
buildMaxHeap(arr)
const len = arr.length
for (let i = len - 1; i > 0; i--) {
swap(arr, 0, i)
heapifyMax(arr, 0, i)
}
} else {
buildMinHeap(arr)
const len = arr.length
for (let i = len - 1; i > 0; i--) {
swap(arr, 0, i)
heapifyMin(arr, 0, i)
}
}
return arr
}
複製代碼
限定爲非負數
計數排序(Counting sort)是一種穩定的線性時間排序算法。該算法於 1954 年由 Harold H. Seward 提出。計數排序使用一個額外的數組 C ,其中第 i 個元素是待排序數組 A 中值等於 i 的元素的個數。而後根據數組 C 來將 A 中的元素排到正確的位置。
當輸入的元素是 n 個 0 到 k 之間的整數時,它的運行時間是 t(n+k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。
因爲用來計數的數組 C 的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上 1),這使得計數排序對於數據範圍很大的數組,須要大量時間和內存。例如:計數排序是用來排序 0 到 100 之間的數字的最好的算法,可是它不適合按字母順序排序人名。可是,計數排序能夠用在基數排序算法中,可以更有效的排序數據範圍很大的數組。
算法的步驟以下:
export function countingSort(arr: at) {
const bucket: at = []
const len = arr.length
// 數組下標的遊標
let sortIndex: number = 0
for (let i = 0; i < len; i++) {
if (bucket[arr[i]]) {
bucket[arr[i]]++
} else {
// 數組的下標不能爲負數,因此計數排序限制只能排序天然數
bucket[arr[i]] = 1
}
}
for (let j = 0; j < bucket.length; j++) {
while (bucket[j]) {
arr[sortIndex++] = j
bucket[j]--
}
}
return arr
}
複製代碼
基數排序(英語:Radix sort)是一種非比較型整數排序算法,其原理是將整數按位數切割成不一樣的數字,而後按每一個位數分別比較。因爲整數也能夠表達字符串(好比名字或日期)和特定格式的浮點數,因此基數排序也不是隻能使用於整數。基數排序的發明能夠追溯到 1887 年赫爾曼·何樂禮在打孔卡片製表機(Tabulation Machine)上的貢獻。
它是這樣實現的:將全部待比較數值(正整數)統一爲一樣的數字長度,數字較短的數前面補零。而後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成之後,數列就變成一個有序序列。
基數排序的方式能夠採用 LSD(Least significant digital)或 MSD(Most significant digital),LSD 的排序方式由鍵值的最右邊開始,而 MSD 則相反,由鍵值的最左邊開始。
基數排序的時間複雜度是 O(k*n),其中 n 是排序元素個數,k 是數字位數。這不是說這個時間複雜度必定優於 O(nlogn),k 的大小取決於數字位的選擇(好比比特位數),和待排序數據所屬數據類型的全集的大小;k 決定了進行多少輪處理,而 n 是每輪處理的操做數目。
export function radixSort(arr: at): at {
const len = arr.length
const max = Math.max(...arr)
let buckets: at[] = []
let digit = `${max}`.length
let start = 1
let res: at = arr.slice()
while (digit > 0) {
start *= 10
for (let i = 0; i < len; i++) {
const j = res[i] % start
if (buckets[j] === void 0) {
buckets[j] = []
}
buckets[j].push(res[i])
}
res = []
for (let j = 0; j < buckets.length; j++) {
buckets[j] && (res = res.concat(buckets[j]))
}
buckets = []
digit--
}
return res
}
複製代碼
桶排序(Bucket sort)或所謂的箱排序,是一個排序算法,工做的原理是將數組分到有限數量的桶裏。每一個桶再個別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序)。桶排序是鴿巢排序的一種概括結果。當要被排序的數組內的數值是均勻分配的時候,桶排序使用線性時間(O(n))。但桶排序並非比較排序,他不受到 O(nlogn)下限的影響。
桶排序如下列程序進行:
export function bucketSort(arr: at, size: number = 5) {
const len = arr.length
const max = Math.max(...arr)
const min = Math.min(...arr)
const bucketSize = Math.floor((max - min) / size) + 1
const bucket: at[] = []
const res: at = []
for (let i = 0; i < len; i++) {
const j = Math.floor((arr[i] - min) / bucketSize)
!bucket[j] && (bucket[j] = [])
bucket[j].push(arr[i])
let l = bucket[j].length
while (l > 0) {
// 每一個桶內部要進行排序
// 冒泡已經很快了,其實只有一個元素須要肯定本身的位置
bucket[j][l] < bucket[j][l - 1] && swap(bucket[j], l, l - 1)
// 不要直接這麼一個排序,bucket[j]內部都是有序的,只有最後一個是無序的
// bucket[j].sort((a, b) => a - b)
l--
}
}
// 每一個桶內部數據已是有序的
// 將桶內數組拼接起來便可
for (let i = 0; i < bucket.length; i++) {
const l = bucket[i] ? bucket[i].length : 0
for (let j = 0; j < l; j++) {
res.push(bucket[i][j])
}
}
return res
}
複製代碼
測試條件是
for (let i = 0; i < 100000; i++) {
arr.push(Math.floor(Math.random() * 10000))
}
複製代碼
測試結果
我測了不少遍,發現計數排序的速度是絕對的第一,固然空間上落後了,若是是大量重複度高,間距不大的值能夠考慮。普通快排的速度也是很是快,基數、桶(可能與桶內排序算法速度有關)雖然是非比較類排序,可是速度上並不佔優點,希爾、堆排序速度也是很是快,而選擇、冒泡排序則很是緩慢。
歡迎你們關注個人掘金和公衆號,算法、TypeScript、React 及其生態源碼按期講解。
推薦一個很是好用的多平臺編輯器, 支持直接複製到微信、知乎、掘金、頭條等平臺,樣式可自定義,預約義的也很好看。