算法入门2

116 阅读3分钟

接上 继续选择排序

学算法为什么难

  • 说起来容易做起来难
  • 运行,发现写错,用log调试
  • 再运行,再写错,再调试
  • 运行,没有错,继续想更好的写法
  • 循环
  • 拿起键盘干,动手最重要
  • 就算你看了正确答案,你自己写也会错
  • 没有人可以帮你完成这个过程

minIndex

  • 你永远都有两种写法
  • 递归和循环
  • 当前的minIndex
let minIndex = (numbers)=>numbers.indexOf(min(numbers))
let min = (numbers)=>{
return min(
 [numbers[0],min(numbers.slice(1))]
)
}
  • 缺点:繁琐=>重写

minIndex重写

let minIndex = (numbers)=>{
let index = 0
for(let i=1;i<numbers.length;i++){
 if(numbers[i]<numbers[index]){
 index = i
 }
 }
 return index
}
  • “渣男算法”:遇到小的就换女朋友
  • 一目了然,一听就会,一写就错
  • 写错记得测试几次

所有的递归都可以改写成循环

改写sort

  • 思路不变:每次找到最小的数放前面,然后i++
//循环
let sort = (numbers)=>{
for(let i=0;i<???;i++){
let index = minIndex(numbers)
//index是当前最小数的下标
//index对应的数应该放到i处
swap(numbers,index,i)//把最小的和当前的交换位置 swap还没实现,只是先占位
//index、i都是index的意思,建议i改名
}
}
  • 怎么知道i<???处应该写什么
  • 提前写好minIndex能有效简化问题
  • 用swap占位能有效简化问题

实现swap

let swap = (array,i,j)=>{
 let temp = array[i]
 array[i] =array[j]
 array[j] = temp
 }
 swap(numbers,1,2)

1.jpg

  • 怎么调换气球位置
  • 第三个人介入 代码中的temp

错误地实现swap

let swap = (a,b)=>{
 let temp = a
 a = b
 b = temp
 }
 swap(numbers[1],numbers[2])
  • 你会发现numbers[1]和numbers[2]的值原封不动
  • 因为a、b是简单类型,传参的时候会复制值(在stack里面放着)
  • 而上一个正确代码中的numbers是array,是对象,传参复制地址(在heap里放着)
  • 传值vs传址

分析

  • 怎么知道i<???处应该写什么
  • 暴力分析
let sort = (numbers)=>{
for(let i=0;i<???;i++){
let index = minIndex(numbers)
swap(numbers,index,i)
 }
}

  • 假设numbers的长度为4

1.jpg

  • bug:minIndex范围有问题,每次都在4个数字里找最小
  • let index = minIndex(numbers)有问题
  • 如果上次循环已经找到第一个最小的数字,那么之后找最小数字的时候可以忽略第一个

1.jpg

-let index = minIndex(numbers.slice(i))+1

  • 为什么要+i
  • 如果不加,那么index总是从0算

2.jpg

let sort = (numbers) =>{
for(let i=0;i< numbers.length -1;i++){
 console.log(`----`)//这个log很精髓,下划线防眼花
 console.log(`i:${i}`)
 let index = minIndex(numbers.slice(i))+ i
 console.log(`index:${index}`)
 console.log(`min:${numbers[index]}`)
 if(index!==i){
  swap(numbers,index,i)
  console.log(`swap${index}:${i}`)
  console.log(numbers)
  }
 }
 return numbers
}
//两组辅助代码
let swap = (array,i,j)=>{
 let temp = array[i]
 array[i] = array[j]
 array[j] = temp
}
let minIndex = (numbers) =>{
 let index = 0
 for(let i=1;i<numbers.length;i++){
  if(numbers[i]<numbers[index]){
   index=i
   }
  }
  return index
}
  • 最终代码删掉log
let sort = (numbers) =>{
for(let i=0;i< numbers.length -1;i++){
 let index = minIndex(numbers.slice(i))+ i
 if(index!==i){
  swap(numbers,index,i)
  }
 }
 return numbers
}

总结

  • 所有的递归都能改成循环
  • 循环的时候有很多细节很难想清楚
  • 要动手列出表格找规律
  • 尤其是边界条件很难确定,比如是到length还是length-1
  • 我们没有处理长度为0和1的数组
  • 如果debug:学会看控制台;学会打log;打log时注意加标记

快速排序

递归思路

  • 想象你的体育委员
  • 面对的同学为[12,3,7,21,5,9,4,6]
  • “以某某为基准,小的去前面,大的去后面”
  • 你只需要重复说这句话就能排序
  • 画图说明

1.jpg

let quickSort = arr =>{
 if(arr.length <= 1){return arr;}
 let pivotIndex = Math.floor(arr.length/2);
 let pivot = arr.splice(pivotIndex,1)[0];
 let left = [];
 let right = [];
 for(let i=0;i<arr.length;i++){
  if(arr[i]<pivot){left.push(arr[i]) 
  }else{right.push(arr[i])}  
 }
 return quickSort(left).concat([pivot],quickSort(right))
} //pivot轴

归并排序

递归思路

  • 不以某某为基准
  • 你面对的同学为[12,3,7,21,5,9,4,6]
  • 左边一半排好序,右边一般排好序
  • 然后把左右两边合并(merge)起来
  • 画图说明

1.jpg

2.jpg

let mergeSort = arr =>{
 let k = arr.length 
 if(k===1){return arr}//精髓:默认已经排好序了
 let left = arr.slice(0,Math.floor(k/2))
 let right = arr.slice(Math.floor(k/2))
 return merge(mergeSort(left),mergeSort(right))
}
let merge = (a,b)=>{
 if(a.length === 0) return b
 if(b.length === 0) return a
 return a[0] > b[0] ?
   [b[0]].concat(merge(a,b.slice(1))) :
   [a[0]].concat(merge(a.slice(1),b)
}

计数排序

引入hash哈希表:按AAAA2222...KKKK顺序排扑克牌

3.jpg

思路

  • 用一个哈希表做记录 {A:0;2:0;3:0;...;k:0}
  • 发现数字N就记N:1,如果再次发现N就加1
  • 最后把哈希表的key全部打出来,假设N:m,那么N需要打印m次
  • 画图演示:整个过程就是记录max和key:vaule。从0遍历到max,有的话就打印1次,有两个就打印两次

1.jpg

let countSort = arr =>{
 let hashTable = {},max = 0,result = []
 for(let i=0;i<arr.length;i++){//遍历数组
  if(!(arr[i] in hashTable)){ //收集第i项的值
   hashTable[arr[i]] = 1
  }else{
   hashTable[arr[i] +=1
  }
  if(arr[i]>max){max=arr[i]}  //收集最大值
 }
 for(let j=0;j<=max;j++){//遍历哈希表
 if(j in hashTable){
  result.push(j)
  }
 }
 return result
} //目前代码有bug:如果j出现两次的情况怎么办
for(let j=0;j<=max;j++){//遍历哈希表
 if(j in hashTable){
  for(let i=0;i<hashTable[j];i++){ //j出现几次就push几次
  result.push(j)
  }
 }
}

计数排序的特点

1.数据结构不同

  • 使用了额外的hashTable
  • 只遍历数组一遍(不过还要遍历一次hashTable)
  • 这叫做“用空间换时间”

2.时间复杂度对比

  • 选择排序O(n^2)

1.jpg

  • 快速排序O(n log2 n)

2.jpg

  • 归并排序O(n log2 n)

3.jpg

  • 计数排序O(n + max)

4.jpg

算法总结

  • 心法:战略上藐视敌人,战术上重视敌人
  • 特点:思路简单;细节多;多花表,多画图,多log;如果实在不想陷入Js的细节可以用伪代码

还有哪些排序算法