几种常见的排序算法详解

215 阅读3分钟

快速排序

  1. 在序列中找一个基准数。
  2. 比基准数小的数放在基准数左边组成一个子序列;比基准数大的数放在基准数右边组成一个子序列。
  3. 再递归排序两个子序列。

原地排序与非原地排序

  • 原地排序:在原序列上进行操作
  • 非原地:开辟一块新空间来操作;需要额外的存储空间影响空间和性能 原地排序:把基准暂时性放在序列的结尾,移动小于基准数放在序列开头,大于则接在后面,再移动基准值放在大于基准值的前面

复杂度

  • 平均时间复杂度:nlognnlog n
  • 最优时间复杂度:nlognnlog n
  • 最坏时间复杂度:n2n^2
  • 空间复杂度:根据实现方法、序列本身大小不同而不同(nlognnlog n~n2lognn^2logn

总结

  • 排序算法是不稳定的排序算法。比如:序列中有值相等的两个数,排完序之后位置可能不同。
  • 出现最坏时间复杂度的情况可能是:序列中的数比基准数都大或者都小。
  • 为了避免极端值的出现最好的解决方法是可以让基准数是随机选择的。 代码实现:选序列中间的数为基准进行非原地快速排序
let quickShort = numbers =>{
 if(numbers.length<2){
   return numbers
 }
 let index = Math.floor(numbers.length/2)
     //splice 返回一个数组
 let middle = numbers.splice(index,1)[0]
 console.log('middle:'+middle)  
 let left = []
 let right = []
 for(let i=0;i<numbers.length;i++){
   if(numbers[i]>middle){
        right.push(numbers[i])
      }else{
        left.push(numbers[i])
      }
 }
     //用concat连接左边数组,基准数和右边数组
 console.log(`left:${left}`)
 console.log(`right:${right}`)
 return quickShort(left).concat(middle,quickShort(right))
}
let numbers = [1,12,5,14,9,6]
console.log(quickShort(numbers))

计数排序

稳定的线性时间排序算法。不是比较排序!

  1. 借助哈希表,遍历序列中的每一项放入哈希表作为 key,用 value 存放出现的次数
  2. 并且标记最大值 max
  3. 创建目标数组
  4. 遍历哈希表(0~max),存在序列中的值就存到目标数组中

复杂度

序列是n个0到k之间的整数时:先遍历一个长度为n的序列,再遍历一个长度为k的序列

  • 平均时间复杂度:n+kn+k
  • 最坏时间复杂度:n+kn+k
  • 最优时间复杂度:n+kn+k
  • 空间复杂度:n+kn+k

代码实现:

let countSort=numbers=>{
 let hash={}
 let result=[]
 let max=0
 for(let i=0;i<numbers.length;i++){
   if(numbers[i]>max){
     max=numbers[i]
   }
   if(!(numbers[i] in hash)){   // 如果number[i]在以往操作中不存在哈希表里
     hash[numbers[i]]=1
   }else{
     hash[numbers[i]]+=1
   }
 }
 
 for(let i=0;i<=max;i++){
   if(i in hash){
     for(let j=0;j<hash[i];j++){  // 如果有数存了两次以上也都要push出来
       result.push(i)
     }
   }
 }
 return result
}
let numbers = [1,12,5,14,9,1,6]
console.log(countSort(numbers))

归并排序(先拆再合)

分治法:分隔+集成

  1. 递归的将当前序列分成两半直到每个子序列中只有一个元素
  2. 两两子序列元素进行比较排序即将上一步得到的子序列合并在一起

如何实现第二步操作? 归并操作

递归法

  1. 创建一个空间用来存放两个子序列排好序合并后的序列
  2. 初始化两个指针,分别用来指向两个排好序的子序列第一个元素
  3. 比较两个数的大小,较小的数放入合并空间中。并将指针移动到下一个位置
  4. 重复步骤3,直到指针到达末尾
  5. 将另一序列剩下的元素直接复制到合并空间尾部

迭代法

  1. 将每相邻的两个子序列进行合并
  2. 若序列数不为1则继续1操作

复杂度

  • 平均时间复杂度:nlognnlog n
  • 最优时间复杂度:nlognnlog n
  • 最坏时间复杂度:nlognnlog n
  • 空间复杂度:nn

代码实现:

let mergeShort=numbers=>{
 if(numbers.length<2){
   return numbers
 }
 
 //拆的目的是最终每一个数组只有1个数时两两比较他们的大小,相当于提供一个入口
 
 let left = numbers.slice(0,Math.floor(numbers.length/2))
 let right = numbers.slice(Math.floor(numbers.length/2),numbers.length)
 return merge(mergeShort(left),mergeShort(right))
}
//真正的操作:比较大小进行组合
let merge = (a,b)=>{
 if(a.length===0){
   return b
 }
 if(b.length===0){
   return a
 }
 //当 a 数组的第一个数比 b 数组的第一个数大时 返回一个数组 数组的第一个数为b数组的第一个数
 return a[0]>b[0]?[b[0]].concat(merge(a,b.splice(1))):[a[0]].concat(merge(b,a.splice(1)))
}


let numbers = [2,9,1,7,4,11,8,0,13,6] 
console.log(mergeShort(numbers))

选择排序

在序列中找到最小(大)元素,放在排序序列的起始位置,然后从剩余未排序的元素中继续找最小(大)元素,放在排序序列的末尾,直到排序完毕。 不稳定!

  • 复杂度 n2n^2