算法是计算机世界永恒的主题,现在世界其实是被算法驱动着进步.学习算法并能熟练在自己业务场景中应用各种算法应该是每个程序员必备的素质.从最基础的十大经典排序算法作为开始,开启自己系统学习算法之路!
冒泡排序(Bubble Sort)
思想
- 冒泡排序只会操作相邻的两个元素
- 每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求.如果不满足就让它们俩互换.
- 冒泡排序算法最多进行 n-1次(n 排序数组长度),每次算法执行元素对比次数是n - i -1次(n 排序数组长度, i 当前算法循环次数)
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const bubbleSort = (arr) => { const length = arr.length; if (length <= 1) return; for(let i = 0; i < length -1; i++) { let hasChange = false; for(let j = 0; j < length - i -1; j++) { if(arr[j] > arr[j+1]) { const temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; hasChange = true; } } if(!hasChange) break; } console.log('arr :', arr) }
const arr = [7, 8, 4, 5, 6, 3, 2, 1]; bubbleSort(arr);
|
特点
优点:
- 排序算法基础,简单实用易于理解.
缺点:
- 比较次数多, 效率较低.
插入排序(Insertion Sort) - 直接插入排序
思想
直接插入排序的实现方式是:将未排序的数据,插入到有序序列中!插入的方式是,从后向前扫描有序序列,找到相应的位置插入.
- 从第一个元素开始,该元素被认为已排序的有序序列.
- 取出最后一个有序序列的下一个元素,在已排序的元素中从后向前扫描.
- 如果该元素(已排序)大于新元素(未排序),将该元素移到下一位置.
- 重复步骤3,直到找到已排序的元素小于或等于新元素的位置.
- 将新元素插入到第四步找到的位置.
- 重复步骤2 ~ 5.
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| const insertionSort = arr => { const length = arr.length; if (length <= 0) return; let preIndex, current; for( let i = 1; i < length; i++) { preIndex = i - 1; current = arr[i]; while (preIndex >=0 && arr[preIndex] > current) { arr[preIndex + 1] = arr[preIndex]; --preIndex; } if (preIndex + 1 != i) { arr[preIndex+1] = current; console.log('arr:', arr) } } return arr; }
const array = [5, 4, 3, 2, 1]; console.log("原始 array :", array); insertionSort(array);
|
插入排序 - 拆半插入(Binary Insertion Sort)
拆半插入排序是直接插入排序一种优化算法,它也属于插入排序.
思想
拆半插入排序和直接插入排序都是将未排序的元素和已排序的元素做比较,插入到合适的已排序队列中,它们不同点是,直接插入排序是从已排序队列尾部向前取已排列元素做比较.而拆半插入排序,是取已排序队列中间一个值做比较,然后再取上一部分或下一部分中间值做比较,直到找到正确的位置.
步骤:
- 取 0 ~ i-1的中间点(m=(i-1) >> 1), arr[i] 与 arr[m]进行比较,若 arr[i] < arr[m],则说明待插入的元素arr[i]应该处于数组0
m索引之间;反之,则说明它应该处于数组mi-1索引之间.
- 重复步骤1.每次缩小一半的查找范围,直至找到插入的位置.
- 将数组中插入位置之后的元素全部后移一位.
- 在指定位置插入第i个元素.
注意: x>>1 是位运算中右移运算,表示右移一位,等同于 x除以2再取整,即 x>>1 == Math.floor(x/2).
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| const binaryInsertionSort = arr => { const len = arr.length; if (len <= 1) return; let current,i,j,low,high,m; for(i = 1; i< len; i++) { low = 0; high = i -1; current = arr[i]; while(low<=high) { m = (low+high)>>1; if( arr[i] >= arr[m]) { low = m+1; } else { high = m-1; } } for( j = i; j > low; j--) { arr[j] = arr[j -1]; console.log('array:', JSON.parse(JSON.stringify(arr))) } arr[low] = current; } console.log('array:', JSON.parse(JSON.stringify(arr))) return arr; }
const array = [5, 4, 3, 2, 1]; console.log('原始 array:', array); binaryInsertionSort(array);
|
选择排序(Selection Sort)
思想
选择排序算法的实现思路和插入排序有些类似,将元素划分为已排序区间和未排序区间.选择排序每次从未排序区间中找到最小的元素,将其放到已排序区间的末尾.
步骤:
- 首先在未排序序列中找到最小(大)元素,存放在排序序列的起始位置.
- 再从剩余的未排序序列元素中寻找最小(大)元素,然后放到已排序序列的末尾.
- 重复第二步,直到所有元素排序完成.
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const selectionSort = arr => { const len = arr.length; let minIndex, temp; for(let i = 0; i < len -1; i++) { minIndex = i; for(let j = i+1; j < len; j++) { if(arr[j] < arr[minIndex]){ minIndex = j; } } temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; console.log('array: ', arr); } return arr; }
const array = [5, 4, 3, 2, 1]; console.log('原始array:', array); selectionSort(array);
|
归并排序(Merge Sort)
思想
归并排序是将一个数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了.
步骤,假设一个数组有N个元素:
- 将每对单个元素(默认情况下,已排序)归并为2个元素的有序数组
- 将2个元素的每对有序数组归并成4个元素的有序数组,重复这个过程……
- 最后一步:归并2个N / 2元素的排序数组(为了简化讨论,我们假设N是偶数)以获得完全排序的N个元素数组
归并排序采用分而治之(Divide and Conquer,缩写为D&C)的思想,这是一个强大的解决问题的范例。
分而治之算法通过以下步骤解决(某种类型的)问题 - 比如我们的排序问题:划分步骤:将大的原始问题划分成较小的子问题并递归地解决较小的子问题,
解决步骤:结合较小的子问题的结果来产生较大的原始问题的结果。
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| const mergeSort = arr => { const len = arr.length; if (len < 2) return arr; let middle = len >> 1; let left = arr.slice(0,middle); let right = arr.slice(middle); return merge(mergeSort(left), mergeSort(right)); }
const merge = (left,right) => { const result = []; while(left.length && right.length) { if(left[0] <= right[0]) { result.push(left.shift()); } else { result.push(right.shift()); } while(left.length) result.push(left.shift()); while(right.length) result.push(right.shift()); return result; } }
const arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]; console.time('归并排序耗时'); console.log('arr :', mergeSort(arr)); console.timeEnd('归并排序耗时');
|
算法执行过程如👇图
快速排序 (Quick Sort)
思想
快速排序通过在未排序队列中找一个基准点,通过和基准点比较,将比基准点大的元素放一个数组,比基准点小的元素放另一个数组.再在分类过一次的数组中找基准点重复上面的步骤,直至数组最后长度<=1.这时数组数据已经排序完成,只需要合并.
步骤:
- 先找到一个基准点(一般使用数组的中部或第一个元素),然后数组被该基准点分为两部分,依次与该基准点数据比较,如果比它小,放左边;反之,放右边.
- 左右分别用一个空数组去存储比较后的数据.
- 最后递归执行1、2操作,直到数组长度<=1;
快速排序是效率最高的排序算法之一,但它需要另外声明两个数组,需要使用内存资源更多.
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const quickSort1 = arr => { if (arr.length <= 1) return arr; const midIndex = len >> 1; const midIndexVal = arr.splice(minIndex,1)[0]; const left = []; const right = []; for(let i = 0; i<arr.length; i++) { if(arr[i] < minIndexVal) { left.push(arr[i]); } else { right.push(arr[i]); } return quickSort1(left).concat(midIndexVal, quickSort1(right)); } } const array2 = [5, 4, 3, 2, 1]; console.log('quickSort1 ', quickSort1(array2));
|
上面👆的的实现方式是通过创建两个空数组left
,right
保存小于和大于基准点的值.
也有一种算法,不用创建空数组,直接通过操作原数组实现左右分离,逐步对比合并的操作.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| const quickSort = (arr, left, right) => { let len = arr.length, partitionIndex; left = typeof left != "number" ? 0 : left; right = typeof right != "number" ? len - 1 : right;
if (left < right) { partitionIndex = partition(arr, left, right); console.log("arr", arr); quickSort(arr, left, partitionIndex - 1); quickSort(arr, partitionIndex + 1, right); } return arr; };
const partition = (arr, left, right) => { let pivot = left, index = pivot + 1; for (let i = index; i <= right; i++) { if (arr[i] < arr[pivot]) { swap(arr, i, index); index++; } } swap(arr, pivot, index - 1); return index - 1; };
const swap = (arr, i, j) => { let temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; };
let arr = [5, 1, 6, 3, 9, 4, 6, 2]; console.log('原数组:' arr); console.log(quickSort(arr));
|
上面算法实现的动图是这样的👇. 这样个算法消耗资源会更少,但理解起来会更困难(我是看了好久才明白)
参考
https://segmentfault.com/a/1190000019916376
https://visualgo.net/zh/sorting?slide=1