Appearance
着重掌握的排序算法,主要是以下 5 种:
- 基础排序算法:
- 冒泡排序
- 插入排序
- 选择排序
- 进阶排序算法
- 归并排序
- 快速排序
冒泡排序
记住两层都是等于 0 开始,第二层循环时 j<len -1 -i 为最优即可
最简单版本
[5, 3, 2, 4, 1]
[3, 5, 2, 4, 1]
↑ ↑
[3, 2, 5, 4, 1]
↑ ↑
[3, 2, 4, 5, 1]
↑ ↑
[3, 2, 4, 1, 5]
↑ ↑
思路:不考虑优化:就是利用两层循环,循环对比相邻的两个数到末尾,每次都把较大的往 右移动,每一轮遍历都能把较大的移动到末尾
function bubbleSort(arr) {
// 缓存数组长度
const len = arr.length
// 外层循环用于控制从头到尾的比较+交换到底有多少轮
for(let i=0;i<len;i++) {
// 内层循环用于完成每一轮遍历过程中的重复比较+交换
for(let j=0;j<len-1;j++) {
// 若相邻元素前面的数比后面的大
if(arr[j] > arr[j+1]) {
// 交换两者
[arr[j], arr[j+1]] = [arr[j+1], arr[j]]
// 或者使用
let temp = arr[j]
arr[j] = arr[j + 1]
a[j + 1] = temp
}
}
}
// 返回数组
return arr
}
改进版冒泡排序的编码实现
当走完第 n 轮循环的时候,数组的后 n 个元素就已经是有序的。下面实现不遍历后面排好 序的写法:
function betterBubbleSort(arr) {
const len = arr.length
for(let i=0;i<len;i++) {
// 注意差别在这行,我们对内层循环的范围作了限制
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]]
}
}
}
return arr
}
最好情况下的冒泡排序
有时候数组它本身就是排好序的,这就是最好情况,那么遇到这种情况如何实现 0(n) 的时 间复杂度
通过添加 flag 判断里面那层循环是否做过交换即可
function betterBubbleSort(arr) {
const len = arr.length
for(let i=0;i<len;i++) {
// 区别在这里,我们加了一个标志位
let flag = false
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
}
选择排序
思路:
选择,顾名思义就是有选择的把最小的排到前面。
打个比方,有 5 个数 的数组,一开始先把 5 个当中的最小放第一位,然后把剩余四个的最小放第二位,,,以 此下去就排序好了。
但是,有个关键点就是如何把后面比较的最小结果放到前面位置 ,就是根据你当前遍历的 index,arr[index] = 比较的最小值,也就是交换一下位置即 可
function selectSort(arr) {
// 缓存数组长度
const len = arr.length
// 定义 minIndex,缓存当前区间最小值的索引,注意是索引
let minIndex
// i 是当前排序区间的起点
for(let i = 0; i < len - 1; i++) {
// 初始化 minIndex 为当前区间第一个元素
minIndex = i
// i、j分别定义当前区间的上下界,i是左边界,j是右边界
for(let j = i; j < len; j++) {
// 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j
if(arr[j] < arr[minIndex]) {
minIndex = j
}
}
// 如果 minIndex 对应元素不是目前的头部元素,则交换两者
if(minIndex !== i) {
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
}
}
return arr
}
在时间复杂度这方面,选择排序没有那么多弯弯绕绕:最好情况也好,最坏情况也罢,两者 之间的区别仅仅在于元素交换的次数不同,但都是要走内层循环作比较的。因此选择排序的 三个时间复杂度都对应两层循环消耗的时间量级: O(n^2)。
插入排序
思路:
- 把数组首位看作是有序序列,从 arr[1] 开始遍历
- 遍历时,a[i] 每次都跟前面的做对比,只有当 a[i] < a[i-1] 时,就把这个 a[i]插入 到这个位置中
- 为了不直接改变原数组,我们需要为 i 跟 a[i] 做一个替身,用于遍历。 let j = i;temp = arr[i]
while(j > 0 && arr[j-1] > temp) { // 如果是,则将 j 前面的一个元素后移一位,为 temp 让出位置 arr[j] = arr[j-1] j-- }
编码实现
function insertSort(arr) {
// 缓存数组长度
const len = arr.length
// temp 用来保存当前需要插入的元素
let temp
// i用于标识每次被插入的元素的索引
for(let i = 1;i < len; i++) {
// j用于帮助 temp 寻找自己应该有的定位
let j = i
temp = arr[i]
// 判断 j 前面一个元素是否比 temp 大
while(j > 0 && arr[j-1] > temp) {
// 如果是,则将 j 前面的一个元素后移一位,为 temp 让出位置
arr[j] = arr[j-1]
j--
}
// 循环让位,最后得到的 j 就是 temp 的正确索引
arr[j] = temp
}
return arr
}
插入排序的时间复杂度
- 最好时间复杂度:它对应的数组本身就有序这种情况。此时内层循环只走一次,整体复杂 度取决于外层循环,时间复杂度就是一层循环对应的 O(n)。
- 最坏时间复杂度:它对应的是数组完全逆序这种情况。此时内层循环每次都要移动有序序 列里的所有元素,因此时间复杂度对应的就是两层循环的 O(n^2)
- 平均时间复杂度:O(n^2)
总结
- 1、冒泡排序 ==》对比+交换 ==》最好情况复杂度为 O(n),最坏也是 O(n^2)
- 2、选择排序 ==》选择最小的元素到 当前区间 的最前边 ==》时间复杂度 O(n^2)
- 3、插入排序 ==》选择后面的元素插入到前面的 有序数组 ==》时间复杂度一般为 O(n^2)