介绍
sort()是JS中用来排序的一个方法,在我们学习各种经典排序算法的时候, 发现都“白学”了。其实在代码中一个sort()就可以解决,并且时间复杂度和空间复杂度相对都不会过高。其实sort()不光可以对数组进行排序, 基本数据类型的数组都可以, 并且可以实现对对象数组的排序。
这个sort其实远比我们想象的更加智能,它会基于要排序的数据量选择性能更加优秀的排序算法来实现排序。
原理
为什么sort()都可以排序呢? 而且时间和空间复杂度都还比较优良, 有没有人想过sort()里面是怎么实现的呢?
sort根据数组的长度进行区分的
当你的数组长度是小于一个定值的, 它会直接进行一个插入排序。
当长度大于定值时,有可能会merge排序或者是quick排序, merge和quick会将整个数组进行划分,进行递归,一旦划分的子数组长度小于定值时, 将不再递归划分,直接进行插入排序。为什么会这么排序呢?
原因是,在数据量小的时候,插入排序的常数代价非常低,虽然时间复杂度是O(n平方),但是常数项比较低,此时, 在数据量比较小的时候,优势就展现出来了。
这个定值是多少?有些人说是60,但是我觉得值得去研究。
数据量大的时候用merge或quick排序
那么问题又来而来,当数据量比较大需要进行划分的时候,什么时候用merge,什么时候用quick呢?
这就要取决数组的类型了,当是基本数据类型的时候,会用quick, 当是对象类型的时候会用merge。
因为当是基本数据类型的时候不需要考虑稳定性的因素,但是对象类型就要考虑了, 因为对象不止用来排序的这一个属性, 可能已经在其他属性已经排好序了, 在此番排序之后还需要保证数组的稳定性。
排序算法对比图标
插入排序思路
代码实现
function swap(arr, i, j){ let temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;}function insertSort(arr){ if(!arr || arr.length <=1) { return; } for(let i=1; i< arr.length;i++) { for(let j=i-1; j >= 0; j--) { if(arr[j] > arr[j+1]) { swap(arr, j, j+1); } else { break; } } }}// let arr = [2,5,3,1,4];let arr = [8, 3, 4, 5, 11, 6, 7, 9, 10];insertSort(arr);console.log(arr);
快速排序思路
快速排序是对冒泡排序的一种改进,采用递归分治的方法进行求解。而快排相比冒泡是一种不稳定排序,时间复杂度最坏是O(n^2),平均时间复杂度为O(nlogn),最好情况的时间复杂度为O(nlogn)。
对于快排来说,「基本思想」是这样的
快排需要将序列变成两个部分,就是「序列左边全部小于一个数」,「序列右面全部大于一个数」,然后利用递归的思想再将左序列当成一个完整的序列再进行排序,同样把序列的右侧也当成一个完整的序列进行排序。
其中这个数在这个序列中是可以随机取的,可以取最左边,可以取最右边,当然也可以取随机数。但是「通常」不优化情况我们取最左边的那个数。
代码实现
function swap(arr, i, j){ let temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;}function quickSort(arr, from, to) { // 递归出口 if(from > to) { return; } // 设置基准元素 let flag = arr[from]; // 设置两个哨兵 let left = from; let right = to; while(left < right) { // 1.必须先移动右侧哨兵 while(left < right && arr[right] <= flag){ right--; } // 2. 移动左侧哨兵 while(left < right && arr[left] >= flag){ left++; } if(left < right) { swap(arr, left, right); } } arr[from] = arr[left]; arr[left] = flag; // 递归 quickSort(arr, from, left-1); quickSort(arr, left+1, to);}let arr = [8, 3, 4, 5, 11, 6, 7, 9, 10];quickSort(arr, 0, arr.length-1);console.log(arr);
归并排序原理
归并和快排都是「基于分治算法」的,分治算法其实应用挺多的,很多分治会用到递归,但事实上「分治和递归是两把事」。分治就是分而治之,可以采用递归实现,也可以自己遍历实现非递归方式。而归并排序就是先将问题分解成代价较小的子问题,子问题再采取代价较小的合并方式完成一个排序。
至于归并的思想是这样的:
第一次:整串先进行划分成一个一个单独,第一次是将序列中(
1 2 3 4 5 6---
)两两归并成有序,归并完(xx xx xx xx----
)这样局部有序的序列。第二次就是两两归并成若干四个(
1 2 3 4 5 6 7 8 ----
)「每个小局部是有序的」。就这样一直到最后这个串串只剩一个,然而这个耗费的总次数logn。每次操作的时间复杂的又是
O(n)
。所以总共的时间复杂度为O(nlogn)
.
代码实现
private static void mergesort(int[] array, int left, int right) { int mid=(left+right)/2; if(left<right) { mergesort(array, left, mid); mergesort(array, mid+1, right); merge(array, left,mid, right); }}private static void merge(int[] array, int l, int mid, int r) { int lindex=l;int rindex=mid+1; int team[]=new int[r-l+1]; int teamindex=0; while (lindex<=mid&&rindex<=r) {//先左右比较合并 if(array[lindex]<=array[rindex]) { team[teamindex++]=array[lindex++]; } else { team[teamindex++]=array[rindex++]; } } while(lindex<=mid)//当一个越界后剩余按序列添加即可 { team[teamindex++]=array[lindex++]; } while(rindex<=r) { team[teamindex++]=array[rindex++]; } for(int i=0;i<teamindex;i++) { array[l+i]=team[i]; }}