今天琪琪子总结一下最近看的排序算法,对于一个非科班出身却偏想要成为冲浪少年的我来说,学习算法真滴是从头开始,一把辛酸泪。谈到排序我的小脑瓜里第一个想到的是.sort()函数,arr.sort((a,b)=>{a-b}),这显然是不够的,所以下面记录总结了经典的5种排序方法,包括原理与JS代码以及时间复杂度计算。
冒泡排序
首先,我们在算法题中看到的排序顺序默认为升序排序哦!冒泡法应该是最最最基础的排序法啦,面试必问。
基本思路
输入一个数组,从第一个元素开始,不断比较相邻的两项,如果前一项大于后一项则交换两项位置,否则位置不变。每一轮比较之后(一轮表示从某一个数字开始比较到比较结束)此轮最大的元素放到数组末尾,则若数据长度为n,进行n轮比较就能完成排序。
代码实现冒泡排序法
const popOrder=function (arr) {
const len=arr.length
for (let i=0;i<len;i++){
let flag=false //设置标志位
//注意j的上限,这样可以减少冗余的排序
for (let j=0;j<len-1-i;j++){
if (arr[j]>arr[j+1]){
[arr[j],arr[j+1]]=[arr[j+1],arr[j]]
flag=true
}
}
if (flag==false) return arr //如果标志位没有变化则说明数组本身就是升序排列无需替换
}
return arr
}
复盘一下
1.最坏情况下,需要比较(n(n-1)/2)次,时间复杂度为O(n^2)
2.最好情况下,n次就能得出结果,时间复杂度为O(n)
3.代码里有几个改进的地方,首先第二层遍历次数不为n-1,因为在上一轮遍历中最大的元素已经放在了队尾,不用再比较一次;第二加入flag标志位,让最好情况下的时间复杂度为O(n)
选择排序法
基本思路
一直在找当前范围内的最小值,将最小值放在当前范围内的头部(此时有一个元素交换),不断缩小范围直到排序完成。一看到范围我就想到了索引呢
代码实现选择排序法
const chooseOrder=function (arr) {
const len=arr.length
let minIndex //一定要用索引,定义范围内最小元素的索引,这样以后才能方便交换
//i,j代表范围的左右索引
for (let i=0;i<len-1;i++){
minIndex=i
for (let j=i;j<len;j++){
if (arr[j]<arr[minIndex]){
minIndex=j //记录当前最小元素索引
}
}
//如果当前最小元素索引不是起始元素则要做位置调换
if (minIndex!==i){
[arr[i],arr[minIndex]]=[arr[minIndex],arr[i]]
}
}
return arr
}
复盘一下
1.时间复杂度为O(n^2),因为必须要进行内层循环比较
插入排序法
基本思路
基于有序数列(意思为当前元素之前的序列一定为有序数列),从后往前寻找当前元素在前一个序列中的正确位置,找到相应位置并插入
代码实现插入排序法
const insertOrder=function(arr){
const len=arr.length
let temp
for (let i=1;i<len;i++){
let j=i
temp=arr[i] //一个中间量存储当前要插入的元素
while (j>0&&arr[j-1]>temp){
arr[j]=arr[j-1] //不停的将不符合顺序的元素调整位置,保证为有序数组
j--
}
arr[j]=temp //找到位置
}
return arr
}
复盘一下
1.最坏情况下,时间复杂度为O(n^2),两个内层比较
2.最好情况下,事件复杂度为O(n),不用排序一次循环即可搞定!
归并排序法
前面三种方法的时间复杂度都是O(n^2),好高,那么就要想如何去降低事件复杂度。
分治思想
就是将一个大问题拆解为多个小问题,将小问题一一求解后,将解整合为大问题的解。
我们的归并排序就是用分治思想分为3步
1.分解子问题
把数组一分为二,再将两个子数组继续一分为二,如此持续下去分割,知道子数组中只有一个元素。
2.求解子问题
从粒子度最小的数组开始,对子数组不断地排序,两两合并来保证合并后的数组为有序的。
3.合并子问题的解,解决大问题
当合并的数组大小为原来数组大小则解决掉了排序问题。
这个排序中我们发现不断在重复分割+排序的操作,所以要想到递归和迭代算法!!!这里我们选择递归算法,递归真香(嘻嘻嘻)同时要做到两个有序数组的合并问题。
代码实现归并排序法
const mergeOrder=function (arr) {
const len=arr.length
if (len==1) return arr
const mid=Math.floor(len/2) //分割点
const LeftArr=mergeOrder(arr.slice(0,mid)) //此处用递归,做同一件事
const RightArr=mergeOrder(arr.slice(mid,len))
arr=mergeArr(LeftArr,RightArr)
return arr
}
function mergeArr(arr1,arr2) {
let i=0,j=0
const res=[]
const len1=arr1.length
const len2=arr2.length
while (i<len1&&j<len2){
if (arr1[i]<arr2[j]){
res.push(arr1[i])
i++
}else {
res.push(arr2[j])
j++
}
}
//如果i<len1说明arr1没有被遍历完则直接合并剩下的有序数组即可
if (i<len1){
res.concat(arr1.slice(i))
}else {
res.concat(arr2.slice(j))
}
return res
}
复盘一下
1.有一个tips,当看到不断二分时,要想到事件复杂度与logn相关,此处数组规模为n,所以时间复杂度为O(nlogn).即切分logn轮,合并的时间复杂度为O(n)。
快排法
基本思路
其实也是用分治的思想,只是与归并排序的区别在于它不会将数组真的分割开在合并到新数组上去,而是在原有数组的内部进行排序
https://segmentfault.com/a/1190000017814119
const quickOrder=function (arr) {
if (arr.length=1){
return arr
}
const len=arr.length
const pivotIndex=Math.floor(len/2)
const pivot=arr.splice(pivotIndex,1)[0]
const left=[]
const right=[]
for (let i=0;i<len;i++){
if (arr[i]<pivot){
left.push(arr[i])
}else {
right.push(arr[i])
}
}
return quickSort(left).concat([pivot],quickSort(right))
}
快排法是最重要的呢!加油