本文內容包括:(雙向)冒泡排序、選擇排序、插入排序、快速排序(填坑和交換)、歸併排序、桶排序、基數排序、計數排序(優化)、堆排序、希爾排序。你們能夠在這裏測試代碼。更多 leetcode
的 JavaScript
解法也能夠在個人算法倉庫中找到,歡迎查看~ html
另外附上十大排序的 C++版本,由於寫慣了JavaScript
,因此這個 C++版本寫得有些醜,請不要介意呀。git
若是你以爲有幫助的話,就點個 star 鼓勵鼓勵我吧,蟹蟹😊github
先推薦一個數據結構和算法動態可視化工具,能夠查看各類算法的動畫演示。下面開始正文。面試
經過相鄰元素的比較和交換,使得每一趟循環都能找到未有序數組的最大值或最小值。 算法
最好:O(n)
,只須要冒泡一次數組就有序了。
最壞:O(n²)
平均:O(n²)
shell
function bubbleSort(nums) { for(let i=0, len=nums.length; i<len-1; i++) { // 若是一輪比較中沒有須要交換的數據,則說明數組已經有序。主要是對[5,1,2,3,4]之類的數組進行優化 let mark = true; for(let j=0; j<len-i-1; j++) { if(nums[j] > nums[j+1]) { [nums[j], nums[j+1]] = [nums[j+1], nums[j]]; mark = false; } } if(mark) return; } }
普通的冒泡排序在一趟循環中只能找出一個最大值或最小值,雙向冒泡則是多一輪循環既找出最大值也找出最小值。數組
function bubbleSort_twoWays(nums) { let low = 0; let high = nums.length - 1; while(low < high) { let mark = true; // 找到最大值放到右邊 for(let i=low; i<high; i++) { if(nums[i] > nums[i+1]) { [nums[i], nums[i+1]] = [nums[i+1], nums[i]]; mark = false; } } high--; // 找到最小值放到左邊 for(let j=high; j>low; j--) { if(nums[j] < nums[j-1]) { [nums[j], nums[j-1]] = [nums[j-1], nums[j]]; mark = false; } } low++; if(mark) return; } }
和冒泡排序類似,區別在於選擇排序是將每個元素和它後面的元素進行比較和交換。 數據結構
最好:O(n²)
最壞:O(n²)
平均:O(n²)
app
function selectSort(nums) { for(let i=0, len=nums.length; i<len; i++) { for(let j=i+1; j<len; j++) { if(nums[i] > nums[j]) { [nums[i], nums[j]] = [nums[j], nums[i]]; } } } }
以第一個元素做爲有序數組,其後的元素經過在這個已有序的數組中找到合適的位置並插入。數據結構和算法
最好:O(n)
,原數組已是升序的。
最壞:O(n²)
平均:O(n²)
function insertSort(nums) { for(let i=1, len=nums.length; i<len; i++) { let temp = nums[i]; let j = i; while(j >= 0 && temp < nums[j-1]) { nums[j] = nums[j-1]; j--; } nums[j] = temp; } }
選擇一個元素做爲基數(一般是第一個元素),把比基數小的元素放到它左邊,比基數大的元素放到它右邊(至關於二分),再不斷遞歸基數左右兩邊的序列。
最好:O(n * logn)
,全部數均勻分佈在基數的兩邊,此時的遞歸就是不斷地二分左右序列。
最壞:O(n²)
,全部數都分佈在基數的一邊,此時劃分左右序列就至關因而插入排序。
平均:O(n * logn)
參考學習連接:
算法 3:最經常使用的排序——快速排序
三種快速排序以及快速排序的優化
從右邊向中間推動的時候,遇到小於基數的數就賦給左邊(一開始是基數的位置),右邊保留原先的值等以後被左邊的值填上。
function quickSort(nums) { // 遞歸排序基數左右兩邊的序列 function recursive(arr, left, right) { if(left >= right) return; let index = partition(arr, left, right); recursive(arr, left, index - 1); recursive(arr, index + 1, right); return arr; } // 將小於基數的數放到基數左邊,大於基數的數放到基數右邊,並返回基數的位置 function partition(arr, left, right) { // 取第一個數爲基數 let temp = arr[left]; while(left < right) { while(left < right && arr[right] >= temp) right--; arr[left] = arr[right]; while(left < right && arr[left] < temp) left++; arr[right] = arr[left]; } // 修改基數的位置 arr[left] = temp; return left; } recursive(nums, 0, nums.length-1); }
從左右兩邊向中間推動的時候,遇到不符合的數就兩邊交換值。
function quickSort1(nums) { function recursive(arr, left, right) { if(left >= right) return; let index = partition(arr, left, right); recursive(arr, left, index - 1); recursive(arr, index + 1, right); return arr; } function partition(arr, left, right) { let temp = arr[left]; let p = left + 1; let q = right; while(p <= q) { while(p <= q && arr[p] < temp) p++; while(p <= q && arr[q] > temp) q--; if(p <= q) { [arr[p], arr[q]] = [arr[q], arr[p]]; // 交換值後兩邊各向中間推動一位 p++; q--; } } // 修改基數的位置 [arr[left], arr[q]] = [arr[q], arr[left]]; return q; } recursive(nums, 0, nums.length-1); }
遞歸將數組分爲兩個序列,有序合併這兩個序列。
最好:O(n * logn)
最壞:O(n * logn)
平均:O(n * logn)
參考學習連接:
圖解排序算法(四)之歸併排序
function mergeSort(nums) { // 有序合併兩個數組 function merge(l1, r1, l2, r2) { let arr = []; let index = 0; let i = l1, j = l2; while(i <= r1 && j <= r2) { arr[index++] = nums[i] < nums[j] ? nums[i++] : nums[j++]; } while(i <= r1) arr[index++] = nums[i++]; while(j <= r2) arr[index++] = nums[j++]; // 將有序合併後的數組修改回原數組 for(let t=0; t<index; t++) { nums[l1 + t] = arr[t]; } } // 遞歸將數組分爲兩個序列 function recursive(left, right) { if(left >= right) return; // 比起(left+right)/2,更推薦下面這種寫法,能夠避免數溢出 let mid = parseInt((right - left) / 2) + left; recursive(left, mid); recursive(mid+1, right); merge(left, mid, mid+1, right); return nums; } recursive(0, nums.length-1); }
取 n 個桶,根據數組的最大值和最小值確認每一個桶存放的數的區間,將數組元素插入到相應的桶裏,最後再合併各個桶。
最好:O(n)
,每一個數都在分佈在一個桶裏,這樣就不用將數插入排序到桶裏了(相似於計數排序以空間換時間)。
最壞:O(n²)
,全部的數都分佈在一個桶裏。
平均:O(n + k)
,k表示桶的個數。
參考學習連接:
拜託,面試別再問我桶排序了!!!
function bucketSort(nums) { // 桶的個數,只要是正數便可 let num = 5; let max = Math.max(...nums); let min = Math.min(...nums); // 計算每一個桶存放的數值範圍,至少爲1, let range = Math.ceil((max - min) / num) || 1; // 建立二維數組,第一維表示第幾個桶,第二維表示該桶裏存放的數 let arr = Array.from(Array(num)).map(() => Array().fill(0)); nums.forEach(val => { // 計算元素應該分佈在哪一個桶 let index = parseInt((val - min) / range); // 防止index越界,例如當[5,1,1,2,0,0]時index會出現5 index = index >= num ? num - 1 : index; let temp = arr[index]; // 插入排序,將元素有序插入到桶中 let j = temp.length - 1; while(j >= 0 && val < temp[j]) { temp[j+1] = temp[j]; j--; } temp[j+1] = val; }) // 修改回原數組 let res = [].concat.apply([], arr); nums.forEach((val, i) => { nums[i] = res[i]; }) }
使用十個桶 0-9,把每一個數從低位到高位根據位數放到相應的桶裏,以此循環最大值的位數次。但只能排列正整數,由於遇到負號和小數點沒法進行比較。
最好:O(n * k)
,k表示最大值的位數。
最壞:O(n * k)
平均:O(n * k)
參考學習連接:
算法總結系列之五: 基數排序(Radix Sort)
function radixSort(nums) { // 計算位數 function getDigits(n) { let sum = 0; while(n) { sum++; n = parseInt(n / 10); } return sum; } // 第一維表示位數即0-9,第二維表示裏面存放的值 let arr = Array.from(Array(10)).map(() => Array()); let max = Math.max(...nums); let maxDigits = getDigits(max); for(let i=0, len=nums.length; i<len; i++) { // 用0把每個數都填充成相同的位數 nums[i] = (nums[i] + '').padStart(maxDigits, 0); // 先根據個位數把每個數放到相應的桶裏 let temp = nums[i][nums[i].length-1]; arr[temp].push(nums[i]); } // 循環判斷每一個位數 for(let i=maxDigits-2; i>=0; i--) { // 循環每個桶 for(let j=0; j<=9; j++) { let temp = arr[j] let len = temp.length; // 根據當前的位數i把桶裏的數放到相應的桶裏 while(len--) { let str = temp[0]; temp.shift(); arr[str[i]].push(str); } } } // 修改回原數組 let res = [].concat.apply([], arr); nums.forEach((val, index) => { nums[index] = +res[index]; }) }
以數組元素值爲鍵,出現次數爲值存進一個臨時數組,最後再遍歷這個臨時數組還原回原數組。由於 JavaScript 的數組下標是以字符串形式存儲的,因此計數排序能夠用來排列負數,但不能夠排列小數。
最好:O(n + k)
,k是最大值和最小值的差。
最壞:O(n + k)
平均:O(n + k)
function countingSort(nums) { let arr = []; let max = Math.max(...nums); let min = Math.min(...nums); // 裝桶 for(let i=0, len=nums.length; i<len; i++) { let temp = nums[i]; arr[temp] = arr[temp] + 1 || 1; } let index = 0; // 還原原數組 for(let i=min; i<=max; i++) { while(arr[i] > 0) { nums[index++] = i; arr[i]--; } } }
把每個數組元素都加上 min
的相反數,來避免特殊狀況下的空間浪費,經過這種優化能夠把所開的空間大小從 max+1
下降爲 max-min+1
,max
和 min
分別爲數組中的最大值和最小值。
好比數組 [103, 102, 101, 100]
,普通的計數排序須要開一個長度爲 104 的數組,並且前面 100 個值都是 undefined
,使用該優化方法後能夠只開一個長度爲 4 的數組。
function countingSort(nums) { let arr = []; let max = Math.max(...nums); let min = Math.min(...nums); // 加上最小值的相反數來縮小數組範圍 let add = -min; for(let i=0, len=nums.length; i<len; i++) { let temp = nums[i]; temp += add; arr[temp] = arr[temp] + 1 || 1; } let index = 0; for(let i=min; i<=max; i++) { let temp = arr[i+add]; while(temp > 0) { nums[index++] = i; temp--; } } }
根據數組創建一個堆(相似徹底二叉樹),每一個結點的值都大於左右結點(最大堆,一般用於升序),或小於左右結點(最小堆,一般用於降序)。對於升序排序,先構建最大堆後,交換堆頂元素(表示最大值)和堆底元素,每一次交換都能獲得未有序序列的最大值。從新調整最大堆,再交換堆頂元素和堆底元素,重複 n-1 次後就能獲得一個升序的數組。
最好:O(n * logn)
,logn是調整最大堆所花的時間。
最壞:O(n * logn)
平均:O(n * logn)
參考學習連接:
常見排序算法 - 堆排序 (Heap Sort)
圖解排序算法(三)之堆排序
function heapSort(nums) { // 調整最大堆,使index的值大於左右節點 function adjustHeap(nums, index, size) { // 交換後可能會破壞堆結構,須要循環使得每個父節點都大於左右結點 while(true) { let max = index; let left = index * 2 + 1; // 左節點 let right = index * 2 + 2; // 右節點 if(left < size && nums[max] < nums[left]) max = left; if(right < size && nums[max] < nums[right]) max = right; // 若是左右結點大於當前的結點則交換,並再循環一遍判斷交換後的左右結點位置是否破壞了堆結構(比左右結點小了) if(index !== max) { [nums[index], nums[max]] = [nums[max], nums[index]]; index = max; } else { break; } } } // 創建最大堆 function buildHeap(nums) { // 注意這裏的頭節點是從0開始的,因此最後一個非葉子結點是 parseInt(nums.length/2)-1 let start = parseInt(nums.length / 2) - 1; let size = nums.length; // 從最後一個非葉子結點開始調整,直至堆頂。 for(let i=start; i>=0; i--) { adjustHeap(nums, i, size); } } buildHeap(nums); // 循環n-1次,每次循環後交換堆頂元素和堆底元素並從新調整堆結構 for(let i=nums.length-1; i>0; i--) { [nums[i], nums[0]] = [nums[0], nums[i]]; adjustHeap(nums, 0, i); } }
經過某個增量 gap,將整個序列分給若干組,從後往前進行組內成員的比較和交換,隨後逐步縮小增量至 1。希爾排序相似於插入排序,只是一開始向前移動的步數從 1 變成了 gap。
最好:O(n * logn)
,步長不斷二分。
最壞:O(n * logn)
平均:O(n * logn)
參考學習連接:
圖解排序算法(二)之希爾排序
function shellSort(nums) { let len = nums.length; // 初始步數 let gap = parseInt(len / 2); // 逐漸縮小步數 while(gap) { // 從第gap個元素開始遍歷 for(let i=gap; i<len; i++) { // 逐步其和前面其餘的組成員進行比較和交換 for(let j=i-gap; j>=0; j-=gap) { if(nums[j] > nums[j+gap]) { [nums[j], nums[j+gap]] = [nums[j+gap], nums[j]]; } else { break; } } } gap = parseInt(gap / 2); } }
看完後若是你們有什麼疑問或發現一些錯誤,能夠在下方留言呀,或者在個人倉庫裏 提issues,咱們一塊兒討論討論😊