每天學習一點點 編程PDF電子書、視頻教程免費下載:
http://www.shitanlife.com/code
一直很懼怕算法,總是感覺特別傷腦子,因此至今為止,幾種基本的排序算法一直都不是很清楚,更別說時間復雜度、空間復雜度什么的了。
今天抽空理了一下,其實感覺還好,並沒有那么可怕,雖然代碼寫出來還是磕磕絆絆,但是思想和原理還是大致上摸清楚了,記錄、分享。
另一篇文章:三種非比較排序算法總結
說明
關於排序,前輩們已經講解的夠多了,我這里主要摘錄一些概念。
排序算法分類
- 比較排序,時間復雜度為O(nlogn) ~ O(n^2),主要有:冒泡排序,選擇排序,插入排序,歸並排序,堆排序,快速排序等
- 非比較排序,時間復雜度可以達到O(n),主要有:計數排序,基數排序,桶排序等
排序穩定性
排序算法穩定性的簡單形式化定義為:如果Ai = Aj,排序前Ai在Aj之前,排序后Ai還在Aj之前,則稱這種排序算法是穩定的。
選擇排序
選擇排序每次比較的是數組中特定索引的值與全數組中每個值的大小比較,每次都選出一個最小(最大)值,如果當前索引的值大於之后索引的值,則兩者進行交換
// 分類 -------------- 內部比較排序 // 數據結構 ---------- 數組 // 最差時間復雜度 ---- O(n^2) // 最優時間復雜度 ---- O(n^2) // 平均時間復雜度 ---- O(n^2) // 所需輔助空間 ------ O(1) // 穩定性 ------------ 不穩定 var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6]; var temp; for (var i = 0; i < arr.length; i++) { for (var j = i + 1; j < arr.length; j++) { if (arr[i] > arr[j]) { temp = arr[j]; arr[j] = arr[i]; arr[i] = temp; } } } console.log(arr);
過程大致如下:
1 4 5 2 3 9 0 7 6 0 4 5 2 3 9 1 7 6 0 2 5 4 3 9 1 7 6 0 1 5 4 3 9 2 7 6 0 1 4 5 3 9 2 7 6 0 1 3 5 4 9 2 7 6 0 1 2 5 4 9 3 7 6 0 1 2 4 5 9 3 7 6 0 1 2 3 5 9 4 7 6 0 1 2 3 4 9 5 7 6 0 1 2 3 4 5 9 7 6 0 1 2 3 4 5 7 9 6 0 1 2 3 4 5 6 9 7 0 1 2 3 4 5 6 7 9
冒泡排序
冒泡排序每次從數組的最開始索引處與后一個值進行比較,如果當前值比較大,則交換位置。這樣一次循環下來,最大的值就會排入到最后的位置。
// 分類 -------------- 內部比較排序 // 數據結構 ---------- 數組 // 最差時間復雜度 ---- O(n^2) // 最優時間復雜度 ---- 如果能在內部循環第一次運行時,使用一個旗標來表示有無需要交換的可能,可以把最優時間復雜度降低到O(n) // 平均時間復雜度 ---- O(n^2) // 所需輔助空間 ------ O(1) // 穩定性 ------------ 穩定 var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6]; var t; for (var m = 0; m < arr.length; m++) { for (var n = 0; n < arr.length - m; n++) { if (arr[n] > arr[n + 1]) { t = arr[n + 1]; arr[n + 1] = arr[n]; arr[n] = t; } } } console.log(arr);
過程大致如下:
1 4 5 2 3 9 0 7 6 1 4 2 5 3 9 0 7 6 1 4 2 3 5 9 0 7 6 1 4 2 3 5 0 9 7 6 1 4 2 3 5 0 7 9 6 1 4 2 3 5 0 7 6 9 1 2 4 3 5 0 7 6 9 1 2 3 4 5 0 7 6 9 1 2 3 4 0 5 7 6 9 1 2 3 4 0 5 6 7 9 1 2 3 0 4 5 6 7 9 1 2 0 3 4 5 6 7 9 1 0 2 3 4 5 6 7 9 0 1 2 3 4 5 6 7 9
插入排序
插入排序類似於撲克牌的插入方法,選取待排列數組中的任意一個數字作為已排序的基准,再依次從待排序數組中取出數字,根據依次比較,將這個數字插入到已排序的數組中
// 分類 ------------- 內部比較排序 // 數據結構 ---------- 數組 // 最差時間復雜度 ---- 最壞情況為輸入序列是降序排列的,此時時間復雜度O(n^2) // 最優時間復雜度 ---- 最好情況為輸入序列是升序排列的,此時時間復雜度O(n) // 平均時間復雜度 ---- O(n^2) // 所需輔助空間 ------ O(1) // 穩定性 ------------ 穩定 var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6]; /** * 直接使用同一個數組方式 */ for (var i = 1; i < arr.length; i++) { var get = arr[i]; var j = i - 1; // 倒敘比較已經排序的值和取到的值進行比較 // 如果取到的值在已經排序中的值中存在合適的索引插入,則需要將這個索引之后的值進行后移 while (j >= 0 && arr[j] > get) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = get; } console.log(arr); /** * 引入一個新的數組方式 * 引入一個數組后會更好理解 */ var sortList = [arr[0]]; for (var i = 1; i < arr.length; i++) { var sLen = sortList.length; // 如果取出的數字比已經排序的第一個值都小,則插入到最開始 if (arr[i] < sortList[0]) { sortList.unshift(arr[i]) continue; } // 如果取出的數字比已經排序的最后一個值都大,則插入到最末尾 if (arr[i] > sortList[sLen - 1]) { sortList[sLen] = arr[i]; continue; } for (var j = 0; j < sLen - 1; j++) { if (arr[i] >= sortList[j] && arr[i] <= sortList[j + 1]) { sortList.splice(j + 1, 0, arr[i]); break; } } } console.log(sortList);
過程大致如下:
1 1 4 1 4 5 1 2 4 5 1 2 3 4 5 1 2 3 4 5 9 0 1 2 3 4 5 9 0 1 2 3 4 5 7 9 0 1 2 3 4 5 6 7 9
二分插入排序
二分插入排序是直接插入排序的一個變種,利用二分查找法找出下一個插入數字對應的索引,然后進行插入。
當n較大時,二分插入排序的比較次數比直接插入排序的最差情況好得多,但比直接插入排序的最好情況要差,所當以元素初始序列已經接近升序時,直接插入排序比二分插入排序比較次數少。二分插入排序元素移動次數與直接插入排序相同,依賴於元素初始序列。
// 分類 -------------- 內部比較排序 // 數據結構 ---------- 數組 // 最差時間復雜度 ---- O(n^2) // 最優時間復雜度 ---- O(nlogn) // 平均時間復雜度 ---- O(n^2) // 所需輔助空間 ------ O(1) // 穩定性 ------------ 穩定 var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6]; /** * 直接使用同一個數組方式 */ for (var i = 1; i < arr.length; i++) { var get = arr[i]; var left = 0; var right = i - 1; // 每次找出中間位置然后進行比較,最終確定索引位置 while (left <= right) { var mid = parseInt((left + right) / 2); if (arr[mid] > get) { right = mid - 1; } else { left = mid + 1; } } for (var k = i - 1; k >= left; k--) { arr[k + 1] = arr[k]; } arr[left] = get; } /** * 引入一個新的數組方式 * 引入一個數組后會更好理解變化的方式 */ var sortList = [arr[0]]; for (var i = 1; i < arr.length; i++) { var sLen = sortList.length; var get = arr[i]; var left = 0; var right = sLen - 1; // 每次找出中間位置然后進行比較,最終確定索引位置 while (left <= right) { var mid = parseInt((left + right) / 2); if (sortList[mid] > get) { right = mid - 1; } else { left = mid + 1; } } // splice是數組插入值的一個快捷方式,將值移位的方式如下 // sortList.splice(left, 0, get); for (var k = sLen - 1; k >= left; k--) { sortList[k + 1] = sortList[k]; } sortList[left] = get; } console.log(sortList);
過程大致如下:
1 1 4 1 4 5 1 2 4 5 1 2 3 4 5 1 2 3 4 5 9 0 1 2 3 4 5 9 0 1 2 3 4 5 7 9 0 1 2 3 4 5 6 7 9
希爾排序
希爾排序是一種更高效的插入排序,通過設計步長(gap)將數組分組,然后每組中單獨采用排序算法將每組排序,然后在縮小步長,進行重復的分組排序工作,直到gap變為1的時候,整個數組分為一組,算法結束。
例如:數組 [1, 4, 5, 2, 3, 9, 0, 7, 6]
,如果每次以數組長度的一半來作為步長,可以分解為以下步驟
1. gap: Math.floor(9 / 2) = 4; 分為四組,分組為: { 1, 3 }, { 4, 9 }, { 5, 0 }, { 2, 7 } 最后一個數字 6 需要等到第5個數字排序完成,也就是3,可以得出3依舊還處在第4索引的位置,因此最后一個分組為 { 3, 6 } 完成一輪分組以及排序后的數組為:[ 1, 4, 0, 2, 3, 9, 5, 7, 6 ] 2. gap: Math.floor(4 / 2) = 2; 分為兩組,分組為: { 1, 0, 3, 5, 6 }, { 4, 2, 9, 7 } 完成第二輪分組以及排序后的數組為:[ 0, 2, 1, 4, 3, 7, 5, 9, 6 ] 3. gap: Math.floor(2 / 2) = 1; 分為一組,即為:{ 0, 2, 1, 4, 3, 7, 5, 9, 6 } 完成第三輪分組以及排序后的數組為:[ 0, 1, 2, 3, 4, 5, 6, 7, 9 ]
// 分類 -------------- 內部比較排序 // 數據結構 ---------- 數組 // 最差時間復雜度 ---- 根據步長序列的不同而不同。已知最好的為O(n(logn)^2) // 最優時間復雜度 ---- O(n) // 平均時間復雜度 ---- 根據步長序列的不同而不同。 // 所需輔助空間 ------ O(1) // 穩定性 ------------ 不穩定 var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6]; var gap = Math.floor(arr.length / 2); function swap(arr, i, j) { var t; t = arr[j]; arr[j] = arr[i]; arr[i] = t; } for (; gap > 0; gap = Math.floor(gap / 2)) { //從第gap個元素,逐個對其所在組進行直接插入排序操作 for(var i = gap; i < arr.length; i++) { var j = i; // 這里采用的其實是冒泡排序 while(j - gap >= 0 && arr[j] < arr[j-gap]) { //插入排序采用交換法 swap(arr, j, j-gap); j -= gap; } // 或者插入排序 var temp = arr[j]; if (arr[j] < arr[j-gap]) { while (j-gap >= 0 && temp < arr[j-gap]) { arr[j] = arr[j-gap]; j -= gap; } arr[j] = temp; } } } console.log(arr);
過程大致如下:
1 4 5 2 3 9 0 7 6 1 4 0 2 3 9 5 7 6 0 4 1 2 3 9 5 7 6 0 2 1 4 3 9 5 7 6 0 2 1 4 3 7 5 9 6 0 1 2 4 3 7 5 9 6 0 1 2 3 4 7 5 9 6 0 1 2 3 4 5 7 9 6 0 1 2 3 4 5 7 6 9 0 1 2 3 4 5 6 7 9
歸並排序
歸並排序采用的是一種分治思想,將整個數組遞歸
分成若干小組,直到最后組中的個數為1時停止,那么此時再與同一級別的分組數字進行比較,這就是並
的操作。然后向上一層層地進行合並,最終合成一個排序好的數組。
這么講可能有點糊塗,用一個例子分析。比如現在有這兩個排序好的數組
var a = [1, 4, 6, 7, 9]; var b = [2, 3, 5, 8]; var temp = []; // 比較過程如下: // 比較兩個數組中的第一個數字,將數字小的壓進temp數組,同時將這個數字從原數組中刪除 // 第一步 a[0] < b[0] // 得到 a: [4, 6, 7, 9] b: [2, 3, 5, 8] temp: [1] // 第二步 a[0] > b[0] // 得到 a: [4, 6, 7, 9] b: [3, 5, 8] temp: [1, 2] // 第三步 a[0] > b[0] // 得到 a: [4, 6, 7, 9] b: [5, 8] temp: [1, 2, 3] // 中間省略N步 // 第N+1步 a: [9] b: [] temp: [1, 2, 3, 4, 5, 6, 7, 8] // 此時b數組已經為空,則直接歸並 // 得到 a: [] b: [] temp: [1, 2, 3, 4, 5, 6, 7, 8, 9]
注:以上的步驟只是歸並排序遞歸中的最上層的一步,其中下面還會分成很多小的合並步驟。
// 分類 -------------- 內部比較排序 // 數據結構 ---------- 數組 // 最差時間復雜度 ---- O(nlogn) // 最優時間復雜度 ---- O(nlogn) // 平均時間復雜度 ---- O(nlogn) // 所需輔助空間 ------ O(n) // 穩定性 ------------ 穩定 var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6]; var len = arr.length; function mergeArray(arr, first, mid, last, t) { var i = mid, j = last, m = first, n = mid + 1, k = 0; while (m <= mid && n <= last) { if (arr[m] > arr[n]) { t[k++] = arr[n++]; } else { t[k++] = arr[m++]; } } while (m <= i) { t[k++] = arr[m++] } while(n <= j) { t[k++] = arr[n++]; } for (var p = 0; p < k; p++) { arr[first + p] = t[p]; } } function mergeSort(arr, first, last, t) { if (first < last) { var mid = Math.floor((first + last) / 2); mergeSort(arr, first, mid, t); mergeSort(arr, mid + 1, last, t) mergeArray(arr, first, mid, last, t); } } mergeSort(arr, 0, len - 1, []); console.log(arr);
過程大致如下:
1 4 5 2 3 9 0 7 6 1 4 5 2 3 9 0 7 6 1 4 5 2 3 9 0 7 6 1 4 5 2 3 9 0 7 6 1 2 3 4 5 9 0 7 6 1 2 3 4 5 0 9 7 6 1 2 3 4 5 0 9 6 7 1 2 3 4 5 0 6 7 9 0 1 2 3 4 5 6 7 9
快速排序
快速排序的原理是:首先隨機選擇一個值,遍歷整個數組,比這個值小的放在左邊的數組中,比這個值大的放在右邊的數組中,然后再根據上一步得出的左右數組重復上述的操作,直到分出的左右數組長度為1或者0的時候停止。
還是舉個栗子吧:
var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6]; // 1. 選取一個數,我這里取中間的數,即為arr[4] = 3 left: [1, 2, 0] right: [4, 5, 9, 7, 6] // 2. 在左右數組中重復上述操作 left: [1, 2, 0] 取數:left[1] = 2 left-left: [0, 1] // 繼續遞歸 left-right: [] // 遞歸結束,直接返回 right: [4, 5, 9, 7, 6] 取數: right[3] = 9 right-left: [4, 5, 7, 6] // 繼續遞歸 right-right: [] // 遞歸結束,直接返回
在遞歸中排序,然后連接選出的那個數,就完成了整個數組的排序
var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6]; function quickSort(arr) { if (arr.length === 1 || arr.length === 0) { return arr; } var left = []; var right = []; var len = arr.length; var f = 0; var l = len - 1; var mid = Math.floor((f + l) / 2); var midVal = arr[mid]; for (var i = 0; i < len; i++) { if (arr[i] < arr[mid]) { left.push(arr[i]); } else if (arr[i] > arr[mid]) { right.push(arr[i]) } } var leftArr = quickSort(left); var rightArr = quickSort(right); return leftArr.concat(midVal).concat(rightArr); } var result = quickSort(arr); console.log(result);
大致過程如下:
left: 1 2 0 middle: 3 right: 4 5 9 7 6 left: 1 0 middle: 2 right: left: 0 middle: 1 right: left: 4 5 7 6 middle: 9 right: left: 4 middle: 5 right: 7 6 left: 6 middle: 7 right:
堆排序
堆排序是指利用堆這種數據結構所設計的一種選擇排序算法。堆是一種近似完全二叉樹的結構(通常堆是通過一維數組來實現的),並滿足性質:以最大堆(也叫大根堆、大頂堆)為例,其中父結點的值總是大於它的孩子節點。
我們可以很容易的定義堆排序的過程:
- 由輸入的無序數組構造一個最大堆,作為初始的無序區
- 把堆頂元素(最大值)和堆尾元素互換
- 把堆(無序區)的尺寸縮小1,並調用heapAdjust(arr, 0)從新的堆頂元素開始進行堆調整
- 重復步驟2,直到堆的尺寸為1
更多請參看https://www.cnblogs.com/skywang12345/p/3602162.html,這篇文章中進行了很詳細地講解。
var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6]; var len = arr.length; function swap(arr, i, j) { var t = arr[j]; arr[j] = arr[i]; arr[i] = t; } function heapAdjust(arr, i, end) { var left = 2 * i + 1; // 左邊子節點 var right = 2 * i + 2; // 右側子節點 var max = i; if (left < end && arr[left] > arr[max]) { max = left; } if (right < end && arr[right] > arr[max]) { max = right; } if (max !== i) { swap(arr, max, i); heapAdjust(arr, max, end); } } function buildMaxHeap(arr, len) { var sNode = Math.floor(len / 2) - 1; // 第一個需要調整的非葉子節點 for (var i = sNode; i >= 0; i--) { heapAdjust(arr, i, len); } return len; } function heapSort(arr) { var heapSize = buildMaxHeap(arr, len); // 堆(無序區)元素個數大於1,未完成排序 while (heapSize > 1) { // 將堆頂元素與堆的最后一個元素互換,並從堆中去掉最后一個元素 // 此處交換操作很有可能把后面元素的穩定性打亂,所以堆排序是不穩定的排序算法 swap(arr, 0, --heapSize); // 從新的堆頂元素開始向下進行堆調整,時間復雜度O(logn) heapAdjust(arr, 0, heapSize); } } heapSort(arr); console.log(arr);
大致實現如下:
1 4 5 2 3 9 0 7 6 7 6 5 4 3 1 0 2 9 6 4 5 2 3 1 0 7 9 5 4 1 2 3 0 6 7 9 4 3 1 2 0 5 6 7 9 3 2 1 0 4 5 6 7 9 2 0 1 3 4 5 6 7 9 1 0 2 3 4 5 6 7 9 0 1 2 3 4 5 6 7 9