一.选择排序的循环写法
1.minIndex
- 你永远有两种写法
[ 递归 ] 和 [ 循环 ] - minIndex重写新思路
1.有数组numbers,令 index = 0,假设这个下标对应值为最小值
2.让数组第0个和第1个进行比较,如果numbers[0]小于numbers[1],则index不变,反之则令 index = 1
3.以此类推,又让此时的 numbers[index]与numbers[2]做比较,还是看哪个小,决定是否修改index
4.返回 numbers[index]
let minIndex = (numbers) =>{
let index = 0
for(i=1 ; i<numbers.length ; i++){
if(numbers[i] < numbers[index]){
index = i
}
}
return index
}
- 所有的递归都可以改写成循环
2.sort排序
- 思路不变
每次找到最小的数放到前面,然后i++
let sort = (numbers) =>{
for(let i=0; i< numbers.length -1; i++){//i的范围下面会分析
let index = minIndex(numbers.slice(i)) + i
//index 是当前最小数的下标
//当前面已经放好后,就不需要加在一起再找最小,而是从后面几个找最小
//+i 是因为slice后数组的下标整体变小了i 因此要把i加回来
if(index!==i){swap(numbers,index,i)}//swap还没实现,index和i对应的元素交换位置
// index、i都是 index 的意思
}
return numbers
}
- 分析
1.怎么知道 i < ???处应该写什么
2.提前写好 minIndex 能有效简化问题
3.用 swap 占位能有效简化问题 - 解决 分析1 的取值范围
假设number长度为n,n可以尝试着取一个小值(如3或者4),硬推出n和i 的规律关系即可
因此i < numbers.length -1
3.实现swap
let swap = (array,i,j) =>{
let temp = array[i]
array[i] = array[j]
array[j] = temp
}
swap(numbers,1,2)
- JS的解构赋值(同样实现两个数的交换)
let a = 1; let b= 2;
[a,b] = [b,a]
a
b
- 错误实现swap
let swap = (a,b) =>{
let temp = a
a = b
b = temp
}
swap(numbers[1],numbers[2])
- 错误原因
因为其中的a、b是简单类型,传参的时候会复制值,相当于是把numbers[1]和numbers[2]复制给了a和b
而上面的代码中 numbers 是对象,传参的时候复制的是地址 - 传值 V.S. 传址
所有放在Stack中的东西都是直接复制的
如果是在Heap中,则是传递地址
4.总结
- 所有递归都能改成循环
- 循环的时候有很多细节
1.这些细节很难想清楚
2.要动手列出表格表规律
3.尤其是边界条件很难确定(可以举例找规律)
4.我们没有处理长度为0和1的数组 - 如果debug
1.学会看控制台
2.学会打log
常用的log:console.log('----')
这个log很有用,在循环中往往重复执行多次,会打印很多中间值,添加之后----
就像分界线一样,看到一次就知道循环了一次
3.打log的时候注意添加标记(打印出来的是谁的值) - 选择排序:每次选择最大/小的,选完就排完(时间复杂度大概是n^2)
二.快速排序(quick sort)
特点:就是快
1.递归思路
- 以某某为基准
1.想象你是一个体育委员
2.你面对的同学为[12,3,7,21,5,9,4,6]
3.[以某某为基准,小的去前面,大的去后面]
4.只需要重复这句话,就能排序 - 如:以21为基准,小的去前面,大的去后面(21与数组内元素依次比较)
得到[12,3,4,5,9,4,21]
此时,21的位置就是正确的,因为此刻在它前面的都是比它小的,后面的都是比它大的。
指定谁才能固定谁
2.快排的源码
Math.floor(x)
:向下找一个最接近的整数
如 x = 3.5,Math.floor(3.5) = 3
return quickSort(left).concat([pivot],quickSort(right))
左边的数组连接上基准和右边的数组
排序左边连接基准再连接排序右边
let quickSort = arr => {
if(arr.length <= 1){return arr;}
let pivotIndex = Math.floor(arr.length/2);
let pivot = arr.splice(pivotIndex,1)[0];
//从数组中删除基准,会返回删除值,splice的返回值一个属也是一个数组,因此要加[0]
let left = [];
let right = [];
for(i= 0;i<arr.length;i++){
if(arr[i]<pivot){left.push(arr[i])}
else{right.push(arr[i])}
}
//此时得到三个数组:left right pivot
return quickSort(left).concat([pivot],quickSort(right))
}
三、归并排序
1.递归思路
- 不以某某为基准
1.想象你是一个体育委员
2.你面对的同学为[12,3,7,21,5,9,4,6]
3.左边一半排好序,右边一半排好序(排序方法为不断分成一半一半,直到只剩一个默认排好序的)
4.然后把左右两边按照一定方法合并(merge)起来
- 归并排序代码
let mergeSort = arr =>{
if(arr.length === 1){return arr}
let left = arr.slice(0,Math.floor(arr.length/2))
let right = arr.slice(Math.floor(arr.length/2))
return merge(mergeSort(left),mergeSort(right))//调用merge是重点
//代码的主要思想不是排序,只是不断划分把情况归结到 arr.length = 1
}
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))
//递归
}
总结
- 选择排序:每次选择最大/小的放到前面
- 快速排序:每次以某个数字为基准,小的往前大的往后
- 归并排序:每次都分两部分,这两部分默认是排好序的,然后把排好序的两个数组合并
如何把连续数组排序:不断两两区分,到最后只剩下单个的时候就是排好序的(一生二、二生四、四生万物)
四、计数排序
- 思路
1.用一个哈希表做记录
2.发现数字N就记 N:1,如果再次发现 N 就加1
3.最后把哈希表的 key 全部打出来,假设N:m,那么N需要打印m次 - 哈希表:一个对象里面有key和value(哈希表是特殊的对象,里没有函数和隐藏属性)
- 折射到现实问题:
收扑克牌,给扑克牌分堆从 A到K,遇到一张A就放到A的堆里,依次放置,直到 A~K全部都是4张分堆完成,然后依次收起来 - 代码思路
1.记录一个max,再记录一个{key : value}
2.从0遍历到max,存在该数值则将对应相等的key打出来(value是几就打印几次)
3.遍历完则排好序
let countSort = arr =>{
let hashTable = {},max = 0, result = []
for(let i=0; i<arr.length;i++){//遍历数组
if(!(arr[i] in hashTable)){//如果arr[i]不再哈希表里
hashTable[arr[i]] = 1//此时哈希表里,key = arr[i],value = 1
}else{
hashTable[arr[i]] += 1//已经存在了就value+1
}
if(arr[i] > max){max = arr[i]}//每个存入的数都与max比较,max始终为大的值
}
for(let j=0;j<=max;j++){//从0开始遍历哈希表,i可以取到max
if(j in hashTable){
for(let i = 0;i<hashTable[i];i++){//如果value的值不为1,有几个就要放几个进去
result.push(j)//如果j在哈希表里,则j就放到数组result里
}
}
}
return result
}
3.1 计数排序的特点
- 数据结构不同
1.使用了额外的hashTable
2.只遍历数组一遍(不过还要遍历一次hashTable)
3.这叫[用空间换时间]:hashTbale
四种排序的时间复杂度
举一个较大的数组,里面的个数为1000个
- 时间复杂度对比
选择排序O(n^2)
:有n个数字,每次都要进行与剩下的数字比较找最小值,四舍五入比较次数全取为n,因此为 nn
快速排序O(n*log2n)
:只要分成一半,则肯定有个log2n,对于每个数字都会有对比,因此为nlog2n(记住对半分衰减很快,但分割不算次数)
归并拍下O(n*log2n)
:同上
计数排序O(n+max)
:先遍历一个n的数组,再遍历一个长度为max的数组
其他排序
1.冒泡排序
- 从第一二个数字开始,依次两两对比,大的数字往后走
- 向后移动的数字比较时遇到比它更大的,则更大的数字往后走
- 冒泡:泡泡从水底起来始终会越来越大
2.插入排序
- 就像是打牌一样,摸牌插牌,摸完牌后,手里的牌都是排好序的
- 从数组中从前往后依次取一个数字,和前面的对比,比它大就插后面,比它小就插到前面,正好放到一个前面比它小后面比它大
- 复杂度为 n^2(最差的情况和前面的都要比较)
3.希尔排序
相关博文:希尔排序
- 希尔排序也是一种插入排序
- 它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序
- 希尔排序在数组中采用跳跃式分组的策略,通过某个增量(一般取gap=length/2,往后不断除以2)将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。
- 希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后
- 步骤
1.第一次取gap=length/2,例如数组长度为8,分为4组,两两一组(每组内数字相隔4个空)
2.对于4组分别进行插入排序,形成4个两两排好序的数组
3.第二次取gap=(length/2)/2,8个数字分为2组,组内进行插入排序
4.一直到gap=1时,则插入排序结束
4.基数排序
- 特别适合用于多位数排序(一位数两位数三位数)
- 先将个位数进行从0~9的排序,然后依次从底部按顺序拿出排好,这样个位的顺序就是好的
- 再将十位数进行从0~9的排序,然后依次从底部按顺序拿出排好
- 百位。。。千位。。。