十大經典算法導圖
圖片名詞解釋:
n: 數據規模
k:“桶”的個數
In-place: 占用常數內存,不占用額外內存
Out-place: 占用額外內存
1.冒泡排序
1.1 原始人冒泡排序
function bubbleSort3(arr3) {
var low = 0;
var high= arr.length-1; //設置變量的初始值
var tmp,j;
console.time('3.改進后冒泡排序耗時');
while (low < high) {
var pos1 = 0,pos2=0;
for (let i= low; i< high; ++i) { //正向冒泡,找到最大者
if (arr[i]> arr[i+1]) {
tmp = arr[i]; arr[i]=arr[i+1];arr[i+1]=tmp;
pos1 = i ;
}
}
high = pos1;// 記錄上次位置
for (let j=high; j>low; --j) { //反向冒泡,找到最小者
if (arr[j]<arr[j-1]) {
tmp = arr[j]; arr[j]=arr[j-1];arr[j-1]=tmp;
pos2 = j;
}
}
low = pos2; //修改low值
}
console.timeEnd('3.改進后冒泡排序耗時');
return arr3;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bubbleSort3(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50] ;
既然每次記錄位置可以減少計算,兩頭算雙管齊下也能減少計算,那么思考,如果每次記錄位置而且還兩頭算是不是會更加省事呢?(根據1.2,1.3自創)
但是冒泡排序也有弊端,就是兩種極端的情況,一種是數據本來就是正序,那做的就是無用功,另外一種就是反序,不想理你。。。具體怎么弊端想想也就知道了
冒泡排序動圖演示
2.選擇排序

插入排序的原理其實很好理解,可以類比選擇排序。選擇排序時在兩個空間進行,等於說每次從舊的空間選出最值放到新的空間,而插入排序則是在同一空間進行。
function binaryInsertionSort(array) {
console.time('二分插入排序耗時:');
for (var i = 1; i < array.length; i++) {
var key = array[i], left = 0, right = i - 1;
while (left <= right) {
var middle = parseInt((left + right) / 2);
if (key < array[middle]) {
right = middle - 1;
} else {
left = middle + 1;
}
}
for (var j = i - 1; j >= left; j--) {
array[j + 1] = array[j];
}
array[left] = key;
}
console.timeEnd('二分插入排序耗時:');
return array;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(binaryInsertionSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50];
二分法插入排序第一遍讀下去,一臉懵逼,寫的是什么鬼,仔細琢磨一下卻別有一番風味,聽小編慢慢講下去,首先外層循環沒什么疑問,就是簡單的遍歷一遍數組,那么先看while循環,left和right兩個變量可以簡單的類比3.1中的已排序的首末兩個位置,然后選取未排序的第一個值和已排序的中間位置的值進行比較,這樣的話也就是在最壞的情況下每層循環也只是計算了已排序的序列長度的一半的次數,簡而言之就是在無限逼近left和right值,找到未排序第一個值應該在的位置。
還是以梁山排名為例子,在宋江沒有到梁上之前,每個上梁上的人跟已經排過名的從大往小進行比較,然后找到自己的位置,在老大宋江來之后,后續人慢慢多了,然后宋老大就訂了條規矩,就是每個新來的人和已排過名次的位於中間名次的好漢進行比較,勝了往前一位比較,敗了往后一位比較,然后找到自己的位置。好了,while循環解釋完畢,那么下面又多了一條for循環,這又是什么鬼?
不要着急,待小編與你慢慢道來,看不懂沒關系,先看循環體,循環體的意思就是把前一個值給后一個,然后看循環條件是從i-1的位置從后往前依次將前一個元素的值給后一個,先不要管i-1是誰,先問 i 是誰,i 不就是未排序的第一個元素么,不就是我們拿來對已進行排序的元素么,簡而言之不就是新上梁山的好漢么,那么從left值開始到 i-1 的位置依次將前一個元素的值給后一個無非就是空出 left 的位置,left 的位置不就是新上梁上好漢的位置!
插入排序法動圖:
4.希爾排序
希爾排序,直接上圖;
像這個算法看圖理解起來並不是很難,就像比賽一樣,1-6一組,2-7一組,每差5為一組進行比較,之后再每差2為一組進行比較,最后就是兩兩比較,有點類似冒泡算法,但又比冒泡多了一層增量的概念。起初小編看到這個導圖的時候感覺編程挺簡單的,無非就是改變一下增量,這有何難?人吶,都是眼高手低,廢話不多說直接看代碼:
其實代碼並不難理解,小編就不詳解了。
配上動圖加深印象:
6.快速排序
3.1 抽象版快速排序
function quickSort(array, left, right) {
console.time('1.快速排序耗時');
if (left < right) {
var x = array[right], i = left - 1, temp;
for (var j = left; j <= right; j++) {
if (array[j] <= x) {
i++;
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
console.log(array) ;
console.log(left,i) ;
quickSort(array, left, i - 1);
console.log(array)
console.log(i,right)
quickSort(array, i + 1, right);
}
console.timeEnd('1.快速排序耗時');
console.log(array)
return array;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(quickSort(arr,0,arr.length-1));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50];
看完代碼一臉懵逼,這是人寫的么?瞬間覺得自己弱爆了,連別人代碼都看不懂,更別說自己寫了,別着急,一點點拆分看。
先看一個疑問點,函數中的參數有三個,第一個數組,沒得說;第二個是左值,第三個是右值;好,到這里先分析結束,首先給讀者一種什么感覺,就是這個排序算法是從左右兩端依次逼近完成排序的,那么對於這個猜想對不對呢?
接着看,if條件語句中判斷left < right,這沒得說,就是從左到右排序的,而且if 如果不成立直接結束本層循環了,那如果滿足條件呢,直接進入for循環,而且在進入for循環之前先記錄了一個本次循環的末尾值,又設置一個i ,還有一個空變量,都分別又是什么意思呢?
接着看,for循環遍歷本層循環,然后依次和末尾值進行比較,那么可想而知,這個變量x無非就是個基數,好了,算法的亮點來了,就是 i 值,如果本層循環某個元素大於本層循環的基數,那么置換兩者的位置,那么 i 的作用就是計數的作用,而 temp 就是作為交換暫時存儲的介質,然后這樣下來就是把每次本層循環的最大值放到了最后,這樣下來在quickSort(array, left, i - 1);不斷遞歸循環之后,該數組的右邊最小值大於左邊的最大值(這里的左邊和右邊不一定等分),而且左邊的順序已經排好了,然后同理排右邊的部分,這樣下來函數結束之后就完成了排序。(暫時小編能理解的大概就是這種程度了,不當之處,還望博友指點一二)
3.2 形象版快速排序
var quickSort2 = function(arr) {
console.time('2.快速排序耗時');
if (arr.length <= 1) { return arr; }
var pivotIndex = Math.floor(arr.length / 2);
var pivot = arr.splice(pivotIndex, 1)[0];
console.log(pivot)
var left = [];
var right = [];
for (var i = 0; i < arr.length; i++){
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
console.timeEnd('2.快速排序耗時');
return quickSort2(left).concat([pivot], quickSort2(right));
};
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(quickSort2(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50];
看完第一種寫法之后,有種放棄的念頭,不要着急,慢慢撥開迷霧你能感受到快速排序的奇特之處。
廢話不多說直接看代碼,第二種開始還能理解,哦,原來和第一種寫法類似,第二種則是選擇中中間數作為基數進行比較,然后再遍歷比較,把比中間值小的放在left數組,把比中間值大的放在right數組中,這種寫法再簡單不過了,而看到后面return quickSort2(left).concat([pivot], quickSort2(right)); 這是什么鬼?是不是寫錯了,怎么感覺那么不對勁呢?不要懷疑經典,拆分代碼看,哦,原來是不斷把數組細分化,分到數組長度為1的最小單位,然后再把左右兩個數組拼湊起來,試想每層基循環都有左右兩個長度為1的數組,且左數組元素比右數組元素值小,而基循環的基數又是兩基數組元素的中間數,那這不就比較完了嗎,把三者拼湊起來不正是排序后的序列么,使用遞歸依次類推形成最后的數組。就是這么簡單,完畢。
配上一個動圖,第一次看可能會很懵逼,配合代碼多看幾遍或許能明白其巧妙之處。
7.堆排序
這種排序方式呢,理論性太強,看動圖的時候滿臉寫着懵逼,多看幾遍似乎明白了編者的意圖,但是要把這種理論的概念寫成代碼卻不容易,且看代碼:
function heapSort(array) {
console.time('堆排序耗時');
//建堆
var heapSize = array.length, temp;
for (var i = Math.floor(heapSize / 2) - 1; i >= 0; i--) {
heapify(array, i, heapSize);
}
//堆排序
for (var j = heapSize - 1; j >= 1; j--) {
temp = array[0];
array[0] = array[j];
array[j] = temp;
console.log(array)
heapify(array, 0, --heapSize);
}
console.timeEnd('堆排序耗時');
return array;
}
function heapify(arr, x, len) {
var l = 2 * x + 1, r = 2 * x + 2, largest = x, temp;
if (l < len && arr[l] > arr[largest]) {
largest = l;
}
if (r < len && arr[r] > arr[largest]) {
largest = r;
}
if (largest != x) {
temp = arr[x];
arr[x] = arr[largest];
arr[largest] = temp;
console.log(arr)
heapify(arr, largest, len);
}
}
var arr=[91,60,96,13,35,65,46,65,10,30,20,31,77,81,22];
console.log(heapSort(arr));//[10, 13, 20, 22, 30, 31, 35, 46, 60, 65, 65, 77, 81, 91, 96];
這種算法有兩個難點,一是建堆,而是堆排序。首先明白什么是堆,堆其實可以這么理解,類似金字塔,一層有一個元素,兩層有兩個元素,三層有四個元素,每層從數組中取元素,從左到右的順序放到堆相應的位置上,也就是說每一層元素個數為2n-1 ;(n 代表行數),這就完成了建堆。
那么想,堆排序中最后一位不就是2n-m(n代表總行數,m代表差多少位不到完成堆的位數),那該元素的父級是誰,2n-1-m/2,2n-1-m/2是誰?拿總位數除以2就知道了,沒錯就是數組的中間值,這也是編者為什么從中間值入手的原因了。
而對於 l = 2*x +1 與 r = 2*x+2 ,不正是每個父級元素對應的子堆么,每一層的堆排序都能夠把本層的最大值剔除出來,這樣當所有 層循環結束之后,序列也就完成了。
這一點小編覺得和歸並排序有點類似,都是細分到最小單元,從最小單元比較,但是同歸並排序有兩大點不同,一是堆排序並不像歸並那么無序,只是一味的平分數組,而堆排序則是按原始序列排出金字塔式的結構,把最大值一層層往上冒,冒到金字塔最頂端的時候把它踢出來,這樣達到排序的效果。
附動圖,不多看幾遍是看不出來什么門道的:
8.計數排序
計數排序就是遍歷數組記錄數組下的元素出現過多次,然后把這個元素找個位置先安置下來,簡單點說就是以原數組每個元素的值作為新數組的下標,而對應小標的新數組元素的值作為出現的次數,相當於是通過下標進行排序。
看代碼:
function countingSort(array) {
var len = array.length,
B = [],
C = [],
min = max = array[0];
console.time('計數排序耗時');
for (var i = 0; i < len; i++) {
min = min <= array[i] ? min : array[i];
max = max >= array[i] ? max : array[i];
C[array[i]] = C[array[i]] ? C[array[i]] + 1 : 1;
console.log(C)
}
// 計算排序后的元素下標
for (var j = min; j < max; j++) {
C[j + 1] = (C[j + 1] || 0) + (C[j] || 0);
console.log(C)
}
for (var k = len - 1; k >= 0; k--) {
B[C[array[k]] - 1] = array[k];
C[array[k]]--;
console.log(B)
}
console.timeEnd('計數排序耗時');
return B;
}
var arr = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];
console.log(countingSort(arr)); //[1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9];
這種算法的亮點就是在於利用下標存數據,利用數據存出現的次數。然后這種算法還有一個亮點就是第二個循環,計算排序后的下標,也就是說在這個地方已經把每個元素對應在排序后的數組的位置已經確定了,在第三個循環中只需要安插在對應的位置即可!
其實這里小編還另外一種算法,沒有上面那種復雜,小編感覺更容易理解,僅供參考:
function countingSort(array) {
var len = array.length,
B = [],
C = [],
min = max = array[0];
console.time('計數排序耗時');
for (var i = 0; i < len; i++) {
min = min <= array[i] ? min : array[i];
max = max >= array[i] ? max : array[i];
C[array[i]] = C[array[i]] ? C[array[i]] + 1 : 1;
}
for (var k = 0; k <len; k++) {
var length = C[k];
for(var m = 0 ;m <length ; m++){
B.push(k);
}
}
console.timeEnd('計數排序耗時');
return B;
}
var arr = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];
console.log(countingSort(arr)); //[1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9];
思想主要是既然我們已經根據下標進行排序了,C數組的下標對應的數值就是該下標出現的次數,那何不吧該次數作為二層循環的長度遍歷一遍直接推送到新得數組中呢?
附動圖便於理解:
9. 桶排序
一看到這個名字就會覺得奇特,幾個意思,我排序還要再准備幾個桶不成?還真別說,想用桶排序還得真准備幾個桶,但是此桶非彼桶,這個桶是用來裝數據用的。其實桶排序和計數排序還有點類似,計數排序是找一個空數組把值作為下標找到其位置,再把出現的次數給存起來,這似乎看似很完美,但也有局限性,不用小編說相信讀者也能明白,既然計數是把原數組的值當做下標來看待,那么該值必然是整數,那假如出現小數怎么辦?這時候就出現了一種通用版的計數排序——桶排序。
小編覺得桶排序可以這么理解,它是以步長為分隔,將最相近數據分隔在一起,然后再在一個桶里排序。好了,現在有個概念,步長是什么玩意?這么來說吧,比如在知道十位的情況下48和36有比較的必要嗎?顯然沒有,十位就把你干下去了,還比什么?那在這里可以簡單的把步長理解為10,桶排序就是這樣,先把同一級別的分到一組,由同一級別的元素進行排序。
代碼實現:
function bucketSort(array, num) {
if (array.length <= 1) {
return array;
}
var len = array.length, buckets = [], result = [], min = max = array[0], space, n = 0;
var index = Math.floor(len / num) ;
while(index<2){
num--;
index = Math.floor(len / num) ;
}
console.time('桶排序耗時');
for (var i = 1; i < len; i++) {
min = min <= array[i] ? min : array[i];
max = max >= array[i] ? max : array[i];
}
space = (max - min + 1) / num; //步長
for (var j = 0; j < len; j++) {
var index = Math.floor((array[j] - min) / space);
if (buckets[index]) { // 非空桶,插入排序
var k = buckets[index].length - 1;
while (k >= 0 && buckets[index][k] > array[j]) {
buckets[index][k + 1] = buckets[index][k];
k--;
}
buckets[index][k + 1] = array[j];
} else { //空桶,初始化
buckets[index] = [];
buckets[index].push(array[j]);
}
}
while (n < num) {
result = result.concat(buckets[n]);
n++;
}
console.timeEnd('桶排序耗時');
return result;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bucketSort(arr,4));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50];
但是這邊有個坑點,就是桶的數量不能過多,也就說說至少兩個桶!為什么?你試下就知道了!
附圖理解:
10.基數排序
其實基數排序和桶排序挺類似的,都是找一個容器把屬於同一類的元素裝起來,然后進行排序。可以把基數排序類比成已知該序列的最高位,然后以除去相對來說的最低位(可能是個位,可能是十位)剩余的位數為桶數,這樣一來步長就是10或者100了。但是基數排序相對桶排序又有多了一個亮點,那就是基數排序是先排最低位(個位),把最低位一致的放在一個桶里,然后依次取出,再進一位(十位),把十位相同的再放到一個桶里,然后再取出,這樣經過兩次重排序就能得到百位以內的排序序列了,百位,千位也是如此。

基數排序 vs 計數排序 vs 桶排序
這三種排序算法都利用了桶的概念,但對桶的使用方法上有明顯差異:
- 基數排序:根據鍵值的每位數字來分配桶
- 計數排序:每個桶只存儲單一鍵值
- 桶排序:每個桶存儲一定范圍的數值