一次搞定九大排序策略
點(diǎn)擊上方 三分鐘學(xué)前端,關(guān)注公眾號(hào)
回復(fù)交流,加入前端編程面試算法每日一題群
14.1 冒泡排序
原理:
從左到右,相鄰元素進(jìn)行比較,如果前一個(gè)元素值大于后一個(gè)元素值(正序),則交換,這樣一輪下來(lái),將最大的數(shù)在最右邊冒泡出來(lái)。這樣一輪一輪下來(lái),最后實(shí)現(xiàn)從小到大排序。
動(dòng)圖演示:
代碼實(shí)現(xiàn):
function bubbleSort(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
const temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// 改進(jìn)冒泡排序
function bubbleSort1(arr) {
for (let i = 0; i < arr.length; i++) {
// 提前退出冒泡循環(huán)的標(biāo)識(shí)位
let flag = false;
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
const temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = true;
// 表示發(fā)生了數(shù)據(jù)交換
}
}
// 沒(méi)有數(shù)據(jù)交換
if(!flag) break
}
}
// 測(cè)試
let arr = [1, 3, 2, 5, 4]
bubbleSort(arr)
console.log(arr) // [1, 2, 3, 4, 5]
let arr1 = [1, 3, 2, 5, 4]
bubbleSort1(arr1)
console.log(arr1) // [1, 2, 3, 4, 5]
復(fù)雜度分析:
-
時(shí)間復(fù)雜度:最好時(shí)間復(fù)雜度 O(n),平均時(shí)間復(fù)雜度 O(n^2^) -
空間復(fù)雜度:O(1)
14.2 選擇排序
原理
從未排序的序列中找到最大(或最小的)放在已排序序列的末尾(為空則放在起始位置),重復(fù)該操作,知道所有數(shù)據(jù)都已放入已排序序列中。
動(dòng)態(tài)演示

代碼實(shí)現(xiàn)
function selectionSort(arr) {
let length = arr.length,
indexMin
for(let i = 0; i < length - 1; i++) {
indexMin = i
for(let j = i; j < length; j++) {
if(arr[indexMin] > arr[j]) {
indexMin = j
}
}
if(i !== indexMin) {
let temp = arr[i]
arr[i] = arr[indexMin]
arr[indexMin] = temp
}
}
}
// 測(cè)試
let arr = [1, 3, 2, 5, 4]
selectionSort(arr)
console.log(arr) // [1, 2, 3, 4, 5]
復(fù)雜度分析
**時(shí)間復(fù)雜度:**O(n^2^)
**空間復(fù)雜度:**O(1)
14.3 歸并排序
原理
它采用了分治策略,將數(shù)組分成2個(gè)較小的數(shù)組,然后每個(gè)數(shù)組再分成兩個(gè)更小的數(shù)組,直至每個(gè)數(shù)組里只包含一個(gè)元素,然后將小數(shù)組不斷的合并成較大的數(shù)組,直至只剩下一個(gè)數(shù)組,就是排序完成后的數(shù)組序列。
實(shí)現(xiàn)步驟:
-
將原始序列平分成兩個(gè)小數(shù)組 -
判斷小數(shù)組長(zhǎng)度是否為1,不為1則繼續(xù)分裂 -
原始數(shù)組被分稱(chēng)了長(zhǎng)度為1的多個(gè)小數(shù)組,然后合并相鄰小數(shù)組(有序合并) -
不斷合并小數(shù)組,直到合并稱(chēng)一個(gè)數(shù)組,則為排序后的數(shù)組序列
動(dòng)圖演示
代碼實(shí)現(xiàn)
function mergeSort(arr) {
let array = mergeSortRec(arr)
return array
}
// 若分裂后的兩個(gè)數(shù)組長(zhǎng)度不為 1,則繼續(xù)分裂
// 直到分裂后的數(shù)組長(zhǎng)度都為 1,
// 然后合并小數(shù)組
function mergeSortRec(arr) {
let length = arr.length
if(length === 1) {
return arr
}
let mid = Math.floor(length / 2),
left = arr.slice(0, mid),
right = arr.slice(mid, length)
return merge(mergeSortRec(left), mergeSortRec(right))
}
// 順序合并兩個(gè)小數(shù)組left、right 到 result
function merge(left, right) {
let result = [],
ileft = 0,
iright = 0
while(ileft < left.length && iright < right.length) {
if(left[ileft] < right[iright]){
result.push(left[ileft ++])
} else {
result.push(right[iright ++])
}
}
while(ileft < left.length) {
result.push(left[ileft ++])
}
while(iright < right.length) {
result.push(right[iright ++])
}
return result
}
// 測(cè)試
let arr = [1, 3, 2, 5, 4]
console.log(mergeSort(arr)) // [1, 2, 3, 4, 5]
復(fù)雜度分析
**時(shí)間復(fù)雜度:**O(nlog~2~n)
**空間復(fù)雜度:**O(n)
14.4 快速排序
原理
和歸并排序一致,它也使用了分治策略的思想,它也將數(shù)組分成一個(gè)個(gè)小數(shù)組,但與歸并不同的是,它實(shí)際上并沒(méi)有將它們分隔開(kāi)。
快排使用了分治策略的思想,所謂分治,顧名思義,就是分而治之,將一個(gè)復(fù)雜的問(wèn)題,分成兩個(gè)或多個(gè)相似的子問(wèn)題,在把子問(wèn)題分成更小的子問(wèn)題,直到更小的子問(wèn)題可以簡(jiǎn)單求解,求解子問(wèn)題,則原問(wèn)題的解則為子問(wèn)題解的合并。
快排的過(guò)程簡(jiǎn)單的說(shuō)只有三步:
-
首先從序列中選取一個(gè)數(shù)作為基準(zhǔn)數(shù) -
將比這個(gè)數(shù)大的數(shù)全部放到它的右邊,把小于或者等于它的數(shù)全部放到它的左邊 (一次快排 partition) -
然后分別對(duì)基準(zhǔn)的左右兩邊重復(fù)以上的操作,直到數(shù)組完全排序
具體按以下步驟實(shí)現(xiàn):
-
1,創(chuàng)建兩個(gè)指針?lè)謩e指向數(shù)組的最左端以及最右端 -
2,在數(shù)組中任意取出一個(gè)元素作為基準(zhǔn) -
3,左指針開(kāi)始向右移動(dòng),遇到比基準(zhǔn)大的停止 -
4,右指針開(kāi)始向左移動(dòng),遇到比基準(zhǔn)小的元素停止,交換左右指針?biāo)赶虻脑? -
5,重復(fù)3,4,直到左指針超過(guò)右指針,此時(shí),比基準(zhǔn)小的值就都會(huì)放在基準(zhǔn)的左邊,比基準(zhǔn)大的值會(huì)出現(xiàn)在基準(zhǔn)的右邊 -
6,然后分別對(duì)基準(zhǔn)的左右兩邊重復(fù)以上的操作,直到數(shù)組完全排序
注意這里的基準(zhǔn)該如何選擇喃?最簡(jiǎn)單的一種做法是每次都是選擇最左邊的元素作為基準(zhǔn):
但這對(duì)幾乎已經(jīng)有序的序列來(lái)說(shuō),并不是最好的選擇,它將會(huì)導(dǎo)致算法的最壞表現(xiàn)。還有一種做法,就是選擇中間的數(shù)或通過(guò) Math.random() 來(lái)隨機(jī)選取一個(gè)數(shù)作為基準(zhǔn),下面的代碼實(shí)現(xiàn)就是以隨機(jī)數(shù)作為基準(zhǔn)。
代碼實(shí)現(xiàn)
let quickSort = (arr) => {
quick(arr, 0 , arr.length - 1)
}
let quick = (arr, left, right) => {
let index
if(left < right) {
// 劃分?jǐn)?shù)組
index = partition(arr, left, right)
if(left < index - 1) {
quick(arr, left, index - 1)
}
if(index < right) {
quick(arr, index, right)
}
}
}
// 一次快排
let partition = (arr, left, right) => {
// 取中間項(xiàng)為基準(zhǔn)
var datum = arr[Math.floor(Math.random() * (right - left + 1)) + left],
i = left,
j = right
// 開(kāi)始調(diào)整
while(i <= j) {
// 左指針右移
while(arr[i] < datum) {
i++
}
// 右指針左移
while(arr[j] > datum) {
j--
}
// 交換
if(i <= j) {
swap(arr, i, j)
i += 1
j -= 1
}
}
return i
}
// 交換
let swap = (arr, i , j) => {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
// 測(cè)試
let arr = [1, 3, 2, 5, 4]
quickSort(arr)
console.log(arr) // [1, 2, 3, 4, 5]
// 第 2 個(gè)最大值
console.log(arr[arr.length - 2]) // 4
快排是從小到大排序,所以第 k 個(gè)最大值在 n-k 位置上
復(fù)雜度分析
-
時(shí)間復(fù)雜度:O(nlog~2~n) -
空間復(fù)雜度:O(nlog~2~n)
14.5 希爾排序
1959年Shell發(fā)明,第一個(gè)突破 O(n^2^) 的排序算法,是簡(jiǎn)單插入排序的改進(jìn)版。它與插入排序的不同之處在于,它會(huì)優(yōu)先比較距離較遠(yuǎn)的元素。
插入排序
插入排序的工作原理是通過(guò)構(gòu)建有序序列,對(duì)于未排序數(shù)據(jù),在已排序序列中從后向前掃描,找到相應(yīng)位置并插入
代碼實(shí)現(xiàn):
function insertionSort(arr) {
let n = arr.length;
let preIndex, current;
for (let i = 1; i < n; 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;
}
插入算法的核心思想是取未排序區(qū)間中的元素,在已排序區(qū)間中找到合適的插入位置將其插入,并保證已排序區(qū)間數(shù)據(jù)一直有序。重復(fù)這個(gè)過(guò)程,直到未排序區(qū)間中元素為空,算法結(jié)束。
復(fù)雜度分析:
-
時(shí)間復(fù)雜度:O(n^2^) -
空間復(fù)雜度:O(1)
希爾排序
回顧一下上面的插入排序:
-
第一趟插入排序后,我們得到的有效序列長(zhǎng)度為 2 -
第二趟插入排序后,我們得到的有效序列長(zhǎng)度為 3 -
... -
直到這個(gè)序列有序
所以,如果序列足夠亂的話(huà),時(shí)間復(fù)雜度為 O(n^2^)
希爾排序又是如何優(yōu)化的喃?
希爾排序又叫縮小增量排序,就是把數(shù)列進(jìn)行分組(組內(nèi)不停使用插入排序),直至從宏觀上看起來(lái)有序,最后插入排序起來(lái)就容易了(無(wú)須多次移位或交換)。
其中組的數(shù)量稱(chēng)為 增量 ,顯然的是,增量是不斷遞減的(直到增量為1)
那我們有是如何進(jìn)行分組喃?
**往往的:**如果一個(gè)數(shù)列有 8 個(gè)元素,我們第一趟的增量是 4 ,第二趟的增量是 2 ,第三趟的增量是 1 。如果一個(gè)數(shù)列有 18 個(gè)元素,我們第一趟的增量是 9 ,第二趟的增量是 4 ,第三趟的增量是2 ,第四趟的增量是 1
很明顯我們可以用一個(gè)序列來(lái)表示增量:n/2、(n/2)/2、...、1,每次增量都/2
例如:
let arr = [4, 1, 5, 8, 7, 3]
排序前:
-
將該數(shù)組看成三組( Math.floor(arr.length/2)),分別是:[4, 1],[5, 8],[7, 3]
第一趟排序:
-
對(duì)三組數(shù)據(jù)分別進(jìn)行插入排序,因此我們?nèi)齻€(gè)數(shù)組得到的結(jié)果為: [1, 4],[5, 8],[3, 7]
此時(shí)數(shù)組是這樣子的:[1, 4, 5, 8, 3, 7]
第二趟排序:
-
增量減少了,上面增量是 3,此時(shí)增量應(yīng)該為1了,因此把[1, 4, 5, 8, 3, 7]看成一個(gè)數(shù)組(從宏觀上是有序的了),對(duì)其進(jìn)行插入排序,直至有序
代碼實(shí)現(xiàn):
function shellSort(arr) {
let n = arr.length;
for (let gap = Math.floor(n / 2); gap > 0; gap = Math.floor(gap / 2)) {
for (let i = gap; i < n; i++) {
let j = i;
let current = arr[i];
while (j - gap >= 0 && current < arr[j - gap]) {
arr[j] = arr[j - gap];
j = j - gap;
}
arr[j] = current;
}
}
return arr;
}
復(fù)雜度分析:
-
時(shí)間復(fù)雜度:O(nlogn) -
空間復(fù)雜度:O(1)
14.6 計(jì)數(shù)排序
原理
計(jì)數(shù)排序不是基于比較的排序算法,其核心在于將輸入的數(shù)據(jù)值轉(zhuǎn)化為鍵存儲(chǔ)在額外開(kāi)辟的數(shù)組空間中。
作為一種線(xiàn)性時(shí)間復(fù)雜度的排序,計(jì)數(shù)排序要求輸入的數(shù)據(jù)必須是有確定范圍的整數(shù)。它是一種典型的拿空間換時(shí)間的排序算法
代碼實(shí)現(xiàn)
function countingSort(arr, maxValue) => {
// 開(kāi)辟的新的數(shù)組,用于將輸入的數(shù)據(jù)值轉(zhuǎn)化為鍵存儲(chǔ)
var bucket = new Array(maxValue + 1),
sortedIndex = 0,
arrLen = arr.length,
bucketLen = maxValue + 1
// 存儲(chǔ)
for (var i = 0; i < arrLen; i++) {
if (!bucket[arr[i]]) {
bucket[arr[i]] = 0
}
bucket[arr[i]]++
}
// 將數(shù)據(jù)從bucket按順序?qū)懭隺rr中
for (var j = 0; j < bucketLen; j++) {
while(bucket[j]-- > 0) {
arr[sortedIndex++] = j
}
}
return arr
}
復(fù)雜度分析
-
時(shí)間復(fù)雜度:O(n+k) -
空間復(fù)雜度:O(n+k)
14.7 桶排序
原理
桶排序是計(jì)數(shù)排序的升級(jí)版。它也是利用函數(shù)的映射關(guān)系。
桶排序 (Bucket sort)的工作的原理:假設(shè)輸入數(shù)據(jù)服從均勻分布,將數(shù)據(jù)分到有限數(shù)量的桶里,每個(gè)桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續(xù)使用桶排序進(jìn)行排)。
完整步驟:
-
首先使用 arr 來(lái)存儲(chǔ)頻率 -
然后創(chuàng)建一個(gè)數(shù)組(有數(shù)量的桶),將頻率作為數(shù)組下標(biāo),對(duì)于出現(xiàn)頻率不同的數(shù)字集合,存入對(duì)應(yīng)的數(shù)組下標(biāo)(桶內(nèi))即可。
// 桶排序
let bucketSort = (arr) => {
let bucket = [], res = []
arr.forEach((value, key) => {
// 利用映射關(guān)系(出現(xiàn)頻率作為下標(biāo))將數(shù)據(jù)分配到各個(gè)桶中
if(!bucket[value]) {
bucket[value] = [key]
} else {
bucket[value].push(key)
}
})
// 遍歷獲取出現(xiàn)頻率
for(let i = 0;i <= bucket.length - 1;i++){
if(bucket[i]) {
res.push(...bucket[i])
}
}
return res
}
復(fù)雜度分析:
-
時(shí)間復(fù)雜度:O(n) -
空間復(fù)雜度:O(n)
14.8 基數(shù)排序
原理
基數(shù)排序是一種非比較型整數(shù)排序算法,其原理是將整數(shù)按位數(shù)切割成不同的數(shù)字,然后按每個(gè)位數(shù)分別比較。由于整數(shù)也可以表達(dá)字符串(比如名字或日期)和特定格式的浮點(diǎn)數(shù),所以基數(shù)排序也不是只能使用于整數(shù)。
完整步驟:
-
取得數(shù)組中的最大數(shù),并取得位數(shù); -
arr為原始數(shù)組,從最低位開(kāi)始取每個(gè)位組成radix數(shù)組; -
對(duì)radix進(jìn)行計(jì)數(shù)排序(利用計(jì)數(shù)排序適用于小范圍數(shù)的特點(diǎn));
動(dòng)圖演示
代碼實(shí)現(xiàn)
//LSD Radix Sort
var counter = [];
function radixSort(arr, maxDigit) {
var mod = 10;
var dev = 1;
for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
for(var j = 0; j < arr.length; j++) {
var bucket = parseInt((arr[j] % mod) / dev);
if(counter[bucket]==null) {
counter[bucket] = [];
}
counter[bucket].push(arr[j]);
}
var pos = 0;
for(var j = 0; j < counter.length; j++) {
var value = null;
if(counter[j]!=null) {
while ((value = counter[j].shift()) != null) {
arr[pos++] = value;
}
}
}
}
return arr;
}
復(fù)雜度分析
-
時(shí)間復(fù)雜度:基數(shù)排序基于分別排序,分別收集,所以是穩(wěn)定的。但基數(shù)排序的性能比桶排序要略差,每一次關(guān)鍵字的桶分配都需要O(n)的時(shí)間復(fù)雜度,而且分配之后得到新的關(guān)鍵字序列又需要O(n)的時(shí)間復(fù)雜度。假如待排數(shù)據(jù)可以分為d個(gè)關(guān)鍵字,則基數(shù)排序的時(shí)間復(fù)雜度將是O(d*2n) ,當(dāng)然d要遠(yuǎn)遠(yuǎn)小于n,因此基本上還是線(xiàn)性級(jí)別的 -
空間復(fù)雜度:O(n+k),其中k為桶的數(shù)量。一般來(lái)說(shuō)n>>k,因此額外空間需要大概n個(gè)左右
基數(shù)排序 vs 計(jì)數(shù)排序 vs 桶排序
這三種排序算法都利用了桶的概念,但對(duì)桶的使用方法上有明顯差異:
-
基數(shù)排序:根據(jù)鍵值的每位數(shù)字來(lái)分配桶; -
計(jì)數(shù)排序:每個(gè)桶只存儲(chǔ)單一鍵值; -
桶排序:每個(gè)桶存儲(chǔ)一定范圍的數(shù)值;
14.9 堆排序
原理
堆是一棵完全二叉樹(shù),它可以使用數(shù)組存儲(chǔ),并且大頂堆的最大值存儲(chǔ)在根節(jié)點(diǎn)(i=1),所以我們可以每次取大頂堆的根結(jié)點(diǎn)與堆的最后一個(gè)節(jié)點(diǎn)交換,此時(shí)最大值放入了有效序列的最后一位,并且有效序列減1,有效堆依然保持完全二叉樹(shù)的結(jié)構(gòu),然后堆化,成為新的大頂堆,重復(fù)此操作,知道有效堆的長(zhǎng)度為 0,排序完成。
完整步驟為:
-
將原序列(n個(gè))轉(zhuǎn)化成一個(gè)大頂堆 -
設(shè)置堆的有效序列長(zhǎng)度為 n -
將堆頂元素(第一個(gè)有效序列)與最后一個(gè)子元素(最后一個(gè)有效序列)交換,并有效序列長(zhǎng)度減1 -
堆化有效序列,使有效序列重新稱(chēng)為一個(gè)大頂堆 -
重復(fù)以上2步,直到有效序列的長(zhǎng)度為 1,排序完成
動(dòng)圖演示

代碼實(shí)現(xiàn)
function heapSort(items) {
// 構(gòu)建大頂堆
buildHeap(items, items.length-1)
// 設(shè)置堆的初始有效序列長(zhǎng)度為 items.length - 1
let heapSize = items.length - 1
for (var i = items.length - 1; i > 1; i--) {
// 交換堆頂元素與最后一個(gè)有效子元素
swap(items, 1, i);
// 有效序列長(zhǎng)度減 1
heapSize --;
// 堆化有效序列(有效序列長(zhǎng)度為 currentHeapSize,拋除了最后一個(gè)元素)
heapify(items, heapSize, 1);
}
return items;
}
// 原地建堆
// items: 原始序列
// heapSize: 有效序列長(zhǎng)度
function buildHeap(items, heapSize) {
// 從最后一個(gè)非葉子節(jié)點(diǎn)開(kāi)始,自上而下式堆化
for (let i = Math.floor(heapSize/2); i >= 1; --i) {
heapify(items, heapSize, i);
}
}
function heapify(items, heapSize, i) {
// 自上而下式堆化
while (true) {
var maxIndex = i;
if(2*i <= heapSize && items[i] < items[i*2] ) {
maxIndex = i*2;
}
if(2*i+1 <= heapSize && items[maxIndex] < items[i*2+1] ) {
maxIndex = i*2+1;
}
if (maxIndex === i) break;
swap(items, i, maxIndex); // 交換
i = maxIndex;
}
}
function swap(items, i, j) {
let temp = items[i]
items[i] = items[j]
items[j] = temp
}
// 測(cè)試
var items = [,1, 9, 2, 8, 3, 7, 4, 6, 5]
heapSort(items)
// [empty, 1, 2, 3, 4, 5, 6, 7, 8, 9]
測(cè)試成功
復(fù)雜度分析
-
**時(shí)間復(fù)雜度:**建堆過(guò)程的時(shí)間復(fù)雜度是 O(n),排序過(guò)程的時(shí)間復(fù)雜度是O(nlogn),整體時(shí)間復(fù)雜度是O(nlogn) -
空間復(fù)雜度: O(1)
14.10 加深
14.10.1 介紹一下快排原理以及時(shí)間復(fù)雜度,并實(shí)現(xiàn)一個(gè)快排
快排使用了分治策略的思想,所謂分治,顧名思義,就是分而治之,將一個(gè)復(fù)雜的問(wèn)題,分成兩個(gè)或多個(gè)相似的子問(wèn)題,在把子問(wèn)題分成更小的子問(wèn)題,直到更小的子問(wèn)題可以簡(jiǎn)單求解,求解子問(wèn)題,則原問(wèn)題的解則為子問(wèn)題解的合并。
快排的過(guò)程簡(jiǎn)單的說(shuō)只有三步:
-
首先從序列中選取一個(gè)數(shù)作為基準(zhǔn)數(shù) -
將比這個(gè)數(shù)大的數(shù)全部放到它的右邊,把小于或者等于它的數(shù)全部放到它的左邊 (一次快排 partition) -
然后分別對(duì)基準(zhǔn)的左右兩邊重復(fù)以上的操作,直到數(shù)組完全排序
具體按以下步驟實(shí)現(xiàn):
-
1,創(chuàng)建兩個(gè)指針?lè)謩e指向數(shù)組的最左端以及最右端 -
2,在數(shù)組中任意取出一個(gè)元素作為基準(zhǔn) -
3,左指針開(kāi)始向右移動(dòng),遇到比基準(zhǔn)大的停止 -
4,右指針開(kāi)始向左移動(dòng),遇到比基準(zhǔn)小的元素停止,交換左右指針?biāo)赶虻脑? -
5,重復(fù)3,4,直到左指針超過(guò)右指針,此時(shí),比基準(zhǔn)小的值就都會(huì)放在基準(zhǔn)的左邊,比基準(zhǔn)大的值會(huì)出現(xiàn)在基準(zhǔn)的右邊 -
6,然后分別對(duì)基準(zhǔn)的左右兩邊重復(fù)以上的操作,直到數(shù)組完全排序
注意這里的基準(zhǔn)該如何選擇喃?最簡(jiǎn)單的一種做法是每次都是選擇最左邊的元素作為基準(zhǔn):
但這對(duì)幾乎已經(jīng)有序的序列來(lái)說(shuō),并不是最好的選擇,它將會(huì)導(dǎo)致算法的最壞表現(xiàn)。還有一種做法,就是選擇中間的數(shù)或通過(guò) Math.random() 來(lái)隨機(jī)選取一個(gè)數(shù)作為基準(zhǔn),下面的代碼實(shí)現(xiàn)就是以隨機(jī)數(shù)作為基準(zhǔn)。
代碼實(shí)現(xiàn)
let quickSort = (arr) => {
quick(arr, 0 , arr.length - 1)
}
let quick = (arr, left, right) => {
let index
if(left < right) {
// 劃分?jǐn)?shù)組
index = partition(arr, left, right)
if(left < index - 1) {
quick(arr, left, index - 1)
}
if(index < right) {
quick(arr, index, right)
}
}
}
// 一次快排
let partition = (arr, left, right) => {
// 取中間項(xiàng)為基準(zhǔn)
var datum = arr[Math.floor(Math.random() * (right - left + 1)) + left],
i = left,
j = right
// 開(kāi)始調(diào)整
while(i <= j) {
// 左指針右移
while(arr[i] < datum) {
i++
}
// 右指針左移
while(arr[j] > datum) {
j--
}
// 交換
if(i <= j) {
swap(arr, i, j)
i += 1
j -= 1
}
}
return i
}
// 交換
let swap = (arr, i , j) => {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
// 測(cè)試
let arr = [1, 3, 2, 5, 4]
quickSort(arr)
console.log(arr) // [1, 2, 3, 4, 5]
// 第 2 個(gè)最大值
console.log(arr[arr.length - 2]) // 4
快排是從小到大排序,所以第 k 個(gè)最大值在 n-k 位置上
復(fù)雜度分析
-
時(shí)間復(fù)雜度:O(nlogn) -
空間復(fù)雜度:O(nlogn)
更多解答
14.10.2 打亂數(shù)組(洗牌算法)
打亂一個(gè)沒(méi)有重復(fù)元素的數(shù)組。
示例:
// 以數(shù)字集合 1, 2 和 3 初始化數(shù)組。
int[] nums = {1,2,3};
Solution solution = new Solution(nums);
// 打亂數(shù)組 [1,2,3] 并返回結(jié)果。任何 [1,2,3]的排列返回的概率應(yīng)該相同。
solution.shuffle();
// 重設(shè)數(shù)組到它的初始狀態(tài)[1,2,3]。
solution.reset();
// 隨機(jī)返回?cái)?shù)組[1,2,3]打亂后的結(jié)果。
solution.shuffle();
解答:Fisher-Yates 洗牌算法
let Solution = function(nums) {
this.nums = nums
};
Solution.prototype.reset = function() {
return this.nums
};
Solution.prototype.shuffle = function() {
let res = [...this.nums]
let n = res.length
for(let i = n-1; i >= 0; i--) {
let randIndex = Math.floor(Math.random() * (i + 1))
swap(res, randIndex, i)
}
return res
};
let swap = function(arr, i, j) {
const temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
復(fù)雜度分析:
-
時(shí)間復(fù)雜度:O(n) -
空間復(fù)雜度:O(n),需要實(shí)現(xiàn) reset功能,原始數(shù)組必須得保存一份
更多解答
14.10.3 阿里五面:說(shuō)下希爾排序的過(guò)程?希爾排序的時(shí)間復(fù)雜度和空間復(fù)雜度又是多少?
1959年Shell發(fā)明,第一個(gè)突破 O(n^2^) 的排序算法,是簡(jiǎn)單插入排序的改進(jìn)版。它與插入排序的不同之處在于,它會(huì)優(yōu)先比較距離較遠(yuǎn)的元素。
插入排序
插入排序的工作原理是通過(guò)構(gòu)建有序序列,對(duì)于未排序數(shù)據(jù),在已排序序列中從后向前掃描,找到相應(yīng)位置并插入
代碼實(shí)現(xiàn):
function insertionSort(arr) {
let n = arr.length;
let preIndex, current;
for (let i = 1; i < n; 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;
}
插入算法的核心思想是取未排序區(qū)間中的元素,在已排序區(qū)間中找到合適的插入位置將其插入,并保證已排序區(qū)間數(shù)據(jù)一直有序。重復(fù)這個(gè)過(guò)程,直到未排序區(qū)間中元素為空,算法結(jié)束。
復(fù)雜度分析:
-
時(shí)間復(fù)雜度:O(n^2^) -
空間復(fù)雜度:O(1)
希爾排序
回顧一下上面的插入排序:
-
第一趟插入排序后,我們得到的有效序列長(zhǎng)度為 2 -
第二趟插入排序后,我們得到的有效序列長(zhǎng)度為 3 -
... -
直到這個(gè)序列有序
所以,如果序列足夠亂的話(huà),時(shí)間復(fù)雜度為 O(n^2^)
希爾排序又是如何優(yōu)化的喃?
希爾排序又叫縮小增量排序,就是把數(shù)列進(jìn)行分組(組內(nèi)不停使用插入排序),直至從宏觀上看起來(lái)有序,最后插入排序起來(lái)就容易了(無(wú)須多次移位或交換)。
其中組的數(shù)量稱(chēng)為 增量 ,顯然的是,增量是不斷遞減的(直到增量為1)
那我們有是如何進(jìn)行分組喃?
往往的: 如果一個(gè)數(shù)列有 8 個(gè)元素,我們第一趟的增量是 4 ,第二趟的增量是 2 ,第三趟的增量是 1 。如果一個(gè)數(shù)列有 18 個(gè)元素,我們第一趟的增量是 9 ,第二趟的增量是 4 ,第三趟的增量是2 ,第四趟的增量是 1
很明顯我們可以用一個(gè)序列來(lái)表示增量:n/2、(n/2)/2、...、1,每次增量都/2
例如:
let arr = [4, 1, 5, 8, 7, 3]
排序前:
-
將該數(shù)組看成三組( Math.floor(arr.length/2)),分別是:[4, 1],[5, 8],[7, 3]
第一趟排序:
-
對(duì)三組數(shù)據(jù)分別進(jìn)行插入排序,因此我們?nèi)齻€(gè)數(shù)組得到的結(jié)果為: [1, 4],[5, 8],[3, 7]
此時(shí)數(shù)組是這樣子的:[1, 4, 5, 8, 3, 7]
第二趟排序:
-
增量減少了,上面增量是 3,此時(shí)增量應(yīng)該為1了,因此把[1, 4, 5, 8, 3, 7]看成一個(gè)數(shù)組(從宏觀上是有序的了),對(duì)其進(jìn)行插入排序,直至有序
代碼實(shí)現(xiàn):
function shellSort(arr) {
let n = arr.length;
for (let gap = Math.floor(n / 2); gap > 0; gap = Math.floor(gap / 2)) {
for (let i = gap; i < n; i++) {
let j = i;
let current = arr[i];
while (j - gap >= 0 && current < arr[j - gap]) {
arr[j] = arr[j - gap];
j = j - gap;
}
arr[j] = current;
}
}
return arr;
}
復(fù)雜度分析:
-
時(shí)間復(fù)雜度:O(nlogn) -
空間復(fù)雜度:O(1)
更多解答
14.10.4 排序鏈表
在 O(n log n) 時(shí)間復(fù)雜度和常數(shù)級(jí)空間復(fù)雜度下,對(duì)鏈表進(jìn)行排序。
示例 1:
輸入: 4->2->1->3
輸出: 1->2->3->4
示例 2:
輸入: -1->5->3->4->0
輸出: -1->0->3->4->5
解答:采用歸并排序
歸并排序采用了分治策略,將數(shù)組分成2個(gè)較小的數(shù)組,然后每個(gè)數(shù)組再分成兩個(gè)更小的數(shù)組,直至每個(gè)數(shù)組里只包含一個(gè)元素,然后將小數(shù)組不斷的合并成較大的數(shù)組,直至只剩下一個(gè)數(shù)組,就是排序完成后的數(shù)組序列。
對(duì)應(yīng)于鏈表喃?
4->2->1->3
第一步:分割
-
使用快慢指針(雙指針?lè)ǎ?,獲取鏈表的中間節(jié)點(diǎn) -
根據(jù)中間節(jié)點(diǎn),分割成兩個(gè)小鏈表 -
遞歸執(zhí)行上一步,直到小鏈表中只有一個(gè)節(jié)點(diǎn)
第二步:歸并(合并有序鏈表)
代碼實(shí)現(xiàn)
let sortList = function(head) {
return mergeSortRec(head)
}
// 歸并排序
// 若分裂后的兩個(gè)鏈表長(zhǎng)度不為 1,則繼續(xù)分裂
// 直到分裂后的鏈表長(zhǎng)度都為 1,
// 然后合并小鏈表
let mergeSortRec = function (head) {
if(!head || !head.next) {
return head
}
// 獲取中間節(jié)點(diǎn)
let middle = middleNode(head)
// 分裂成兩個(gè)鏈表
let temp = middle.next
middle.next = null
let left = head, right = temp
// 繼續(xù)分裂(遞歸分裂)
left = mergeSortRec(left)
right = mergeSortRec(right)
// 合并兩個(gè)有序鏈表
return mergeTwoLists(left, right)
}
// 獲取中間節(jié)點(diǎn)
// - 如果鏈表長(zhǎng)度為奇數(shù),則返回中間節(jié)點(diǎn)
// - 如果鏈表長(zhǎng)度為偶數(shù),則有兩個(gè)中間節(jié)點(diǎn),這里返回第一個(gè)
let middleNode = function(head) {
let fast = head, slow = head
while(fast && fast.next && fast.next.next) {
slow = slow.next
fast = fast.next.next
}
return slow
}
// 合并兩個(gè)有序鏈表
let mergeTwoLists = function(l1, l2) {
let preHead = new ListNode(-1);
let cur = preHead;
while(l1 && l2){
if(l1.val < l2.val){
cur.next = l1;
l1 = l1.next;
}else{
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = l1 || l2;
return preHead.next;
}
引入遞歸算法的復(fù)雜度分析:
-
遞歸算法的時(shí)間復(fù)雜度:遞歸的總次數(shù) * 每次遞歸的數(shù)量 -
遞歸算法的空間復(fù)雜度:遞歸的深度 * 每次遞歸創(chuàng)建變量的個(gè)數(shù)
復(fù)雜度分析
-
時(shí)間復(fù)雜度:遞歸的總次數(shù)為 T(logn) ,每次遞歸的數(shù)量為 T(n) ,時(shí)間復(fù)雜度為 O(nlogn) -
空間復(fù)雜度:遞歸的深度為 T(logn) ,每次遞歸創(chuàng)建變量的個(gè)數(shù)為 T(c) (c為常數(shù)),空間復(fù)雜度為 O(logn)
關(guān)于復(fù)雜度分析,請(qǐng)看這篇:前端進(jìn)階算法1:如何分析、統(tǒng)計(jì)算法的執(zhí)行效率和資源消耗?
優(yōu)化遞歸
使用迭代代替遞歸,優(yōu)化時(shí)間復(fù)雜度:O(logn) —> O(1)
更多解答
14.10.5 撲克牌問(wèn)題
魔術(shù)師手中有一堆撲克牌,觀眾不知道它的順序,接下來(lái)魔術(shù)師:
-
從牌頂拿出一張牌, 放到桌子上 -
再?gòu)呐祈斈靡粡埮疲?放在手上牌的底部
如此往復(fù)(不斷重復(fù)以上兩步),直到魔術(shù)師手上的牌全部都放到了桌子上。
此時(shí),桌子上的牌順序?yàn)椋?牌頂) 1,2,3,4,5,6,7,8,9,10,11,12,13 (牌底)。
問(wèn):原來(lái)魔術(shù)師手上牌的順序,用函數(shù)實(shí)現(xiàn)。
解答:反向推導(dǎo)
假設(shè),原來(lái)魔術(shù)師手上牌的順序數(shù)組為 origin ,最后放在桌子上的順序數(shù)組為 result
正向的操作為: origin 取出第一個(gè)插入 result 前面, origin 再取出第一個(gè)換到自己的末尾,如此重復(fù);
反向操作為: origin 最后一個(gè)放到自己的第一個(gè)前面, result 拿出第一個(gè)插入 origin 前面,如此重復(fù);
const calc = (arr) => {
const origin = [];
for (let i = 0; i < arr.length; i++) {
if (origin.length) {
const item = origin.pop();
origin.unshift(item);
}
origin.unshift(result[i])
}
return origin;
}
// 測(cè)試
const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
// 原有順序
calc(result)
// [13, 2, 12, 6, 11, 3, 10, 5, 9, 1, 8, 4, 7]
更多解答
14.10.6 有效三角形的個(gè)數(shù)
給定一個(gè)包含非負(fù)整數(shù)的數(shù)組,你的任務(wù)是統(tǒng)計(jì)其中可以組成三角形三條邊的三元組個(gè)數(shù)。
示例 1:
輸入: [2,2,3,4]
輸出: 3
解釋:
有效的組合是:
2,3,4 (使用第一個(gè) 2)
2,3,4 (使用第二個(gè) 2)
2,2,3
注意:
-
數(shù)組長(zhǎng)度不超過(guò)1000。 -
數(shù)組里整數(shù)的范圍為 [0, 1000]。
本題可結(jié)合:
-
字節(jié)&leetcode1:兩數(shù)之和 -
騰訊&leetcode15:三數(shù)之和
一起練習(xí)
解法:排序+雙指針
我們知道三角形的任意兩邊之和大于第三邊,任意兩邊之差小于第三邊,如果這三條邊長(zhǎng)從小到大為 a 、 b 、 c ,當(dāng)且僅當(dāng) a + b > c 這三條邊能組成三角形
解題思路: 先數(shù)組排序,排序完后,固定最長(zhǎng)的邊,利用雙指針?lè)ㄅ袛嗥溆噙?/p>
以 nums[nums.length - 1] 作為最長(zhǎng)的邊 nums[k] ( k = nums.length - 1 )
以 nums[i] 作為最短邊,以 nums[nums.length - 2] 作為第二個(gè)數(shù) nums[j] ( j = nums.length - 2 ) ,
判斷 nums[i] + nums[j] 是否大于 nums[k] ,
-
nums[i] + nums[j] > nums[k],則:nums[i+1] + nums[j] > nums[k]
nums[i+2] + nums[j] > nums[k]
...
nums[j-1] + nums[j] > nums[k]則可構(gòu)成三角形的三元組個(gè)數(shù)加
j-i,并且j往前移動(dòng)一位(j--), 繼續(xù)進(jìn)入下一輪判斷 -
nums[i] + nums[j] <= nums[k],則l往后移動(dòng)一位(nums是增序排列),繼續(xù)判斷
代碼實(shí)現(xiàn):
let triangleNumber = function(nums) {
if(!nums || nums.length < 3) return 0
let count = 0
// 排序
nums.sort((a, b) => a - b)
for(let k = nums.length - 1; k > 1; k--){
let i = 0, j = k - 1
while(i < j){
if(nums[i] + nums[j] > nums[k]){
count += j - i
j--
} else {
i++
}
}
}
return count
}
復(fù)雜度分析:
-
時(shí)間復(fù)雜度:O(n^2^) -
空間復(fù)雜度:O(n)
注意:
關(guān)于 Array.prototype.sort() ,ES 規(guī)范并沒(méi)有指定具體的算法,在 V8 引擎中, 7.0 版本之前,數(shù)組長(zhǎng)度小于10時(shí), Array.prototype.sort() 使用的是插入排序,否則用快速排序。
在 V8 引擎 7.0 版本之后就舍棄了快速排序,因?yàn)樗皇欠€(wěn)定的排序算法,在最壞情況下,時(shí)間復(fù)雜度會(huì)降級(jí)到 O(n2)。
而是采用了一種混合排序的算法:TimSort 。
這種功能算法最初用于Python語(yǔ)言中,嚴(yán)格地說(shuō)它不屬于以上10種排序算法中的任何一種,屬于一種混合排序算法:
在數(shù)據(jù)量小的子數(shù)組中使用插入排序,然后再使用歸并排序將有序的子數(shù)組進(jìn)行合并排序,時(shí)間復(fù)雜度為 O(nlogn) 。
更多解答
最后
號(hào)內(nèi)回復(fù):
120 套模版
