JavaScript:十大排序的演算法思路和程式碼實現
本文內容包括:(雙向)氣泡排序、選擇排序、插入排序、快速排序(填坑和交換)、歸併排序、桶排序、基數排序、計數排序(最佳化)、堆排序、希爾排序。大家可以在這裡測試程式碼。更多 leetcode 的 JavaScript 解法也可以在我的演算法倉庫中找到,歡迎檢視~
氣泡排序
透過相鄰元素的比較和交換,使得每一趟迴圈都能找到未有序陣列的最大值或最小值。
最好:O(n),只需要冒泡一次陣列就有序了。
最壞:O(n²)
平均:O(n²)
單向冒泡
1. function bubbleSort(nums) { 2. for(let i=0, len=nums.length; i<len-1; i++) { 3. // 如果一輪比較中沒有需要交換的資料,則說明陣列已經有序。主要是對[5,1,2,3,4]之類的陣列進行最佳化 4. let mark = true; 5. for(let j=0; j<len-i-1; j++) { 6. if(nums[j] > nums[j+1]) { 7. [nums[j], nums[j+1]] = [nums[j+1], nums[j]]; 8. mark = false; 9. } 10. } 11. if(mark) return; 12. } 13. }
一個人學習會有迷茫,動力不足。這裡推薦一下我的前端學習交流群:731771211 ,裡面都是學習前端的,如果你想製作酷炫的網頁,想學習程式設計。自己整理了一份2019最全面前端學習資料,從最基礎的HTML+CSS+JS【炫酷特效,遊戲,外掛封裝,設計模式】到移動端HTML5的專案實戰的學習資料都有整理,送給每一位前端小夥伴,有想學習web前端的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入學習。
雙向冒泡
普通的氣泡排序在一趟迴圈中只能找出一個最大值或最小值,雙向冒泡則是多一輪迴圈既找出最大值也找出最小值。
1. function bubbleSort_twoWays(nums) { 2. let low = 0; 3. let high = nums.length - 1; 4. while(low < high) { 5. let mark = true; 6. // 找到最大值放到右邊 7. for(let i=low; i<high; i++) { 8. if(nums[i] > nums[i+1]) { 9. [nums[i], nums[i+1]] = [nums[i+1], nums[i]]; 10. mark = false; 11. } 12. } 13. high--; 14. // 找到最小值放到左邊 15. for(let j=high; j>low; j--) { 16. if(nums[j] < nums[j-1]) { 17. [nums[j], nums[j-1]] = [nums[j-1], nums[j]]; 18. mark = false; 19. } 20. } 21. low++; 22. if(mark) return; 23. } 24. }
選擇排序
和氣泡排序相似,區別在於選擇排序是將每一個元素和它後面的元素進行比較和交換。
最好:O(n²)
最壞:O(n²)
平均:O(n²)
1. function selectSort(nums) { 2. for(let i=0, len=nums.length; i<len; i++) { 3. for(let j=i+1; j<len; j++) { 4. if(nums[i] > nums[j]) { 5. [nums[i], nums[j]] = [nums[j], nums[i]]; 6. } 7. } 8. } 9. }
插入排序
以第一個元素作為有序陣列,其後的元素透過在這個已有序的陣列中找到合適的位置並插入。
最好:O(n),原陣列已經是升序的。
最壞:O(n²)
平均:O(n²)
1. function insertSort(nums) { 2. for(let i=1, len=nums.length; i<len; i++) { 3. let temp = nums[i]; 4. let j = i; 5. while(j >= 0 && temp < nums[j-1]) { 6. nums[j] = nums[j-1]; 7. j--; 8. } 9. nums[j] = temp; 10. } 11. }
快速排序
選擇一個元素作為基數(通常是第一個元素),把比基數小的元素放到它左邊,比基數大的元素放到它右邊(相當於二分),再不斷遞迴基數左右兩邊的序列。
最好:O(n * logn),所有數均勻分佈在基數的兩邊,此時的遞迴就是不斷地二分左右序列。
最壞:O(n²),所有數都分佈在基數的一邊,此時劃分左右序列就相當於是插入排序。
平均:O(n * logn)
快速排序之填坑
從右邊向中間推進的時候,遇到小於基數的數就賦給左邊(一開始是基數的位置),右邊保留原先的值等之後被左邊的值填上。
1. function quickSort(nums) { 2. // 遞迴排序基數左右兩邊的序列 3. function recursive(arr, left, right) { 4. if(left >= right) return; 5. let index = partition(arr, left, right); 6. recursive(arr, left, index - 1); 7. recursive(arr, index + 1, right); 8. return arr; 9. } 10. // 將小於基數的數放到基數左邊,大於基數的數放到基數右邊,並返回基數的位置 11. function partition(arr, left, right) { 12. // 取第一個數為基數 13. let temp = arr[left]; 14. while(left < right) { 15. while(left < right && arr[right] >= temp) right--; 16. arr[left] = arr[right]; 17. while(left < right && arr[left] < temp) left++; 18. arr[right] = arr[left]; 19. } 20. // 修改基數的位置 21. arr[left] = temp; 22. return left; 23. } 24. recursive(nums, 0, nums.length-1); 25. }
快速排序之交換
從左右兩邊向中間推進的時候,遇到不符合的數就兩邊交換值。
1. function quickSort1(nums) { 2. function recursive(arr, left, right) { 3. if(left >= right) return; 4. let index = partition(arr, left, right); 5. recursive(arr, left, index - 1); 6. recursive(arr, index + 1, right); 7. return arr; 8. } 9. function partition(arr, left, right) { 10. let temp = arr[left]; 11. let p = left + 1; 12. let q = right; 13. while(p <= q) { 14. while(p <= q && arr[p] < temp) p++; 15. while(p <= q && arr[q] > temp) q--; 16. if(p <= q) { 17. [arr[p], arr[q]] = [arr[q], arr[p]]; 18. // 交換值後兩邊各向中間推進一位 19. p++; 20. q--; 21. } 22. } 23. // 修改基數的位置 24. [arr[left], arr[q]] = [arr[q], arr[left]]; 25. return q; 26. } 27. recursive(nums, 0, nums.length-1); 28. }
歸併排序
遞迴將陣列分為兩個序列,有序合併這兩個序列。
最好:O(n * logn)
最壞:O(n * logn)
平均:O(n * logn)
1. function mergeSort(nums) { 2. // 有序合併兩個陣列 3. function merge(l1, r1, l2, r2) { 4. let arr = []; 5. let index = 0; 6. let i = l1, j = l2; 7. while(i <= r1 && j <= r2) { 8. arr[index++] = nums[i] < nums[j] ? nums[i++] : nums[j++]; 9. } 10. while(i <= r1) arr[index++] = nums[i++]; 11. while(j <= r2) arr[index++] = nums[j++]; 12. // 將有序合併後的陣列修改回原陣列 13. for(let t=0; t<index; t++) { 14. nums[l1 + t] = arr[t]; 15. } 16. } 17. // 遞迴將陣列分為兩個序列 18. function recursive(left, right) { 19. if(left >= right) return; 20. // 比起(left+right)/2,更推薦下面這種寫法,可以避免數溢位 21. let mid = parseInt((right - left) / 2) + left; 22. recursive(left, mid); 23. recursive(mid+1, right); 24. merge(left, mid, mid+1, right); 25. return nums; 26. } 27. recursive(0, nums.length-1); 28. }
桶排序
取 n 個桶,根據陣列的最大值和最小值確認每個桶存放的數的區間,將陣列元素插入到相應的桶裡,最後再合併各個桶。
最好:O(n),每個數都在分佈在一個桶裡,這樣就不用將數插入排序到桶裡了(類似於計數排序以空間換時間)。
最壞:O(n²),所有的數都分佈在一個桶裡。
平均:O(n + k),k表示桶的個數。
1. function bucketSort(nums) { 2. // 桶的個數,只要是正數即可 3. let num = 5; 4. let max = Math.max(...nums); 5. let min = Math.min(...nums); 6. // 計算每個桶存放的數值範圍,至少為1, 7. let range = Math.ceil((max - min) / num) || 1; 8. // 建立二維陣列,第一維表示第幾個桶,第二維表示該桶裡存放的數 9. let arr = Array.from(Array(num)).map(() => Array().fill(0)); 10. nums.forEach(val => { 11. // 計算元素應該分佈在哪個桶 12. let index = parseInt((val - min) / range); 13. // 防止index越界,例如當[5,1,1,2,0,0]時index會出現5 14. indexindex = index >= num ? num - 1 : index; 15. let temp = arr[index]; 16. // 插入排序,將元素有序插入到桶中 17. let j = temp.length - 1; 18. while(j >= 0 && val < temp[j]) { 19. temp[j+1] = temp[j]; 20. j--; 21. } 22. temp[j+1] = val; 23. }) 24. // 修改回原陣列 25. let res = [].concat.apply([], arr); 26. nums.forEach((val, i) => { 27. nums[i] = res[i]; 28. }) 29. }
基數排序
使用十個桶 0-9,把每個數從低位到高位根據位數放到相應的桶裡,以此迴圈最大值的位數次。但只能排列正整數,因為遇到負號和小數點無法進行比較。
最好:O(n * k),k表示最大值的位數。
最壞:O(n * k)
平均:O(n * k)
1. function radixSort(nums) { 2. // 計算位數 3. function getDigits(n) { 4. let sum = 0; 5. while(n) { 6. sum++; 7. n = parseInt(n / 10); 8. } 9. return sum; 10. } 11. // 第一維表示位數即0-9,第二維表示裡面存放的值 12. let arr = Array.from(Array(10)).map(() => Array()); 13. let max = Math.max(...nums); 14. let maxDigits = getDigits(max); 15. for(let i=0, len=nums.length; i<len; i++) { 16. // 用0把每一個數都填充成相同的位數 17. nums[i] = (nums[i] + '').padStart(maxDigits, 0); 18. // 先根據個位數把每一個數放到相應的桶裡 19. let temp = nums[i][nums[i].length-1]; 20. arr[temp].push(nums[i]); 21. } 22. // 迴圈判斷每個位數 23. for(let i=maxDigits-2; i>=0; i--) { 24. // 迴圈每一個桶 25. for(let j=0; j<=9; j++) { 26. let temp = arr[j] 27. let len = temp.length; 28. // 根據當前的位數i把桶裡的數放到相應的桶裡 29. while(len--) { 30. let str = temp[0]; 31. temp.shift(); 32. arr[str[i]].push(str); 33. } 34. } 35. } 36. // 修改回原陣列 37. let res = [].concat.apply([], arr); 38. nums.forEach((val, index) => { 39. nums[index] = +res[index]; 40. }) 41. }
計數排序
以陣列元素值為鍵,出現次數為值存進一個臨時陣列,最後再遍歷這個臨時陣列還原回原陣列。因為 JavaScript 的陣列下標是以字串形式儲存的,所以計數排序可以用來排列負數,但不可以排列小數。
最好:O(n + k),k是最大值和最小值的差。
最壞:O(n + k)
平均:O(n + k)
1. function countingSort(nums) { 2. let arr = []; 3. let max = Math.max(...nums); 4. let min = Math.min(...nums); 5. // 裝桶 6. for(let i=0, len=nums.length; i<len; i++) { 7. let temp = nums[i]; 8. arr[temp] = arr[temp] + 1 || 1; 9. } 10. let index = 0; 11. // 還原原陣列 12. for(let i=min; i<=max; i++) { 13. while(arr[i] > 0) { 14. nums[index++] = i; 15. arr[i]--; 16. } 17. } 18. }
計數排序最佳化
把每一個陣列元素都加上 min 的相反數,來避免特殊情況下的空間浪費,透過這種最佳化可以把所開的空間大小從 max+1 降低為 max-min+1,max 和 min 分別為陣列中的最大值和最小值。
比如陣列 [103, 102, 101, 100],普通的計數排序需要開一個長度為 104 的陣列,而且前面 100 個值都是 undefined,使用該最佳化方法後可以只開一個長度為 4 的陣列。
1. function countingSort(nums) { 2. let arr = []; 3. let max = Math.max(...nums); 4. let min = Math.min(...nums); 5. // 加上最小值的相反數來縮小陣列範圍 6. let add = -min; 7. for(let i=0, len=nums.length; i<len; i++) { 8. let temp = nums[i]; 9. temp += add; 10. arr[temp] = arr[temp] + 1 || 1; 11. } 12. let index = 0; 13. for(let i=min; i<=max; i++) { 14. let temp = arr[i+add]; 15. while(temp > 0) { 16. nums[index++] = i; 17. temp--; 18. } 19. } 20. }
堆排序
根據陣列建立一個堆(類似完全二叉樹),每個結點的值都大於左右結點(最大堆,通常用於升序),或小於左右結點(最小堆,通常用於降序)。對於升序排序,先構建最大堆後,交換堆頂元素(表示最大值)和堆底元素,每一次交換都能得到未有序序列的最大值。重新調整最大堆,再交換堆頂元素和堆底元素,重複 n-1 次後就能得到一個升序的陣列。
最好:O(n * logn),logn是調整最大堆所花的時間。
最壞:O(n * logn)
平均:O(n * logn)
1. function heapSort(nums) { 2. // 調整最大堆,使index的值大於左右節點 3. function adjustHeap(nums, index, size) { 4. // 交換後可能會破壞堆結構,需要迴圈使得每一個父節點都大於左右結點 5. while(true) { 6. let max = index; 7. let left = index * 2 + 1; // 左節點 8. let right = index * 2 + 2; // 右節點 9. if(left < size && nums[max] < nums[left]) max = left; 10. if(right < size && nums[max] < nums[right]) max = right; 11. // 如果左右結點大於當前的結點則交換,並再迴圈一遍判斷交換後的左右結點位置是否破壞了堆結構(比左右結點小了) 12. if(index !== max) { 13. [nums[index], nums[max]] = [nums[max], nums[index]]; 14. index = max; 15. } 16. else { 17. break; 18. } 19. } 20. } 21. // 建立最大堆 22. function buildHeap(nums) { 23. // 注意這裡的頭節點是從0開始的,所以最後一個非葉子結點是 parseInt(nums.length/2)-1 24. let start = parseInt(nums.length / 2) - 1; 25. let size = nums.length; 26. // 從最後一個非葉子結點開始調整,直至堆頂。 27. for(let i=start; i>=0; i--) { 28. adjustHeap(nums, i, size); 29. } 30. } 31. buildHeap(nums); 32. // 迴圈n-1次,每次迴圈後交換堆頂元素和堆底元素並重新調整堆結構 33. for(let i=nums.length-1; i>0; i--) { 34. [nums[i], nums[0]] = [nums[0], nums[i]]; 35. adjustHeap(nums, 0, i); 36. } 37. }
希爾排序
透過某個增量 gap,將整個序列分給若干組,從後往前進行組內成員的比較和交換,隨後逐步縮小增量至 1。希爾排序類似於插入排序,只是一開始向前移動的步數從 1 變成了 gap。
最好:O(n * logn),步長不斷二分。
最壞:O(n * logn)
平均:O(n * logn)
1. function shellSort(nums) { 2. let len = nums.length; 3. // 初始步數 4. let gap = parseInt(len / 2); 5. // 逐漸縮小步數 6. while(gap) { 7. // 從第gap個元素開始遍歷 8. for(let i=gap; i<len; i++) { 9. // 逐步其和前面其他的組成員進行比較和交換 10. for(let j=i-gap; j>=0; j-=gap) { 11. if(nums[j] > nums[j+gap]) { 12. [nums[j], nums[j+gap]] = [nums[j+gap], nums[j]]; 13. } 14. else { 15. break; 16. } 17. } 18. } 19. gap = parseInt(gap / 2); 20. } 21. }
看完後如果大家有什麼疑問,可以在下方留言
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69901074/viewspace-2646529/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 十大經典排序演算法動畫解析和 Java 程式碼實現排序演算法動畫Java
- JavaScript實現常用排序演算法JavaScript排序演算法
- 十大排序演算法Java實現排序演算法Java
- 十大經典排序演算法最強總結(含JAVA程式碼實現)排序演算法Java
- 九種排序演算法的 JavaScript 實現排序演算法JavaScript
- JavaScript實現的7種排序演算法JavaScript排序演算法
- JavaScript實現經典排序演算法JavaScript排序演算法
- 常用排序演算法之JavaScript實現排序演算法JavaScript
- JavaScript實現的9大排序演算法JavaScript排序演算法
- PHP 四種基本排序演算法的程式碼實現PHP排序演算法
- C#實現——十大排序演算法之選擇排序C#排序演算法
- 【演算法】排序04——程式碼簡約而不簡單的希爾排序(含程式碼實現)演算法排序
- 十大排序演算法全面解析 - Java實現排序演算法Java
- 十大排序演算法全面解析-Java實現排序演算法Java
- javascript希爾排序演算法程式碼例項JavaScript排序演算法
- 幾種常用的排序演算法之JavaScript實現排序演算法JavaScript
- 棧的應用場景思路分析和程式碼實現
- javascript排序各種演算法例項程式碼JavaScript排序演算法
- 常見排序演算法原理及JS程式碼實現排序演算法JS
- 幾大排序演算法的理解和程式碼實現(超級詳細的過程)排序演算法
- 六種排序演算法的JavaScript實現以及總結排序演算法JavaScript
- 瀑布流程式碼實現及思路
- 十大經典排序演算法總結(JavaScript描述)排序演算法JavaScript
- JavaScript依賴注入的實現思路JavaScript依賴注入
- 全面瞭解歸併排序演算法及程式碼實現排序演算法
- 快速排序(Quicksort)的Javascript實現排序UIJavaScript
- 【演算法】排序03——看著複雜其實就兩步的堆排序(含程式碼實現)演算法排序
- 排序演算法之快速排序的實現排序演算法
- Java實現氣泡排序和插入排序演算法Java排序演算法
- Java實現十大排序演算法,配合動態圖片Java排序演算法
- javascript實現的放大效果程式碼JavaScript
- 各種排序演算法總結及C#程式碼實現排序演算法C#
- 菜鳥必看的排序演算法(簡單通俗)及程式碼實現,幾張圖帶你吃透排序演算法排序演算法
- 排序演算法:堆排序的實現和時間複雜度分析排序演算法時間複雜度
- python排序演算法的實現-快速排序Python排序演算法
- JavaScript實現:插入排序!!!JavaScript排序
- 【演算法】希爾排序的實現演算法排序
- 中綴轉字尾表示式思路分析和程式碼實現