冒泡排序
冒泡排序就是比较不停的比较的过程
共两层循环, 外面一层实际上是控制次数的, 每循环一次都会有一个最值被交换到了最尾部
那么下次循环的时候就不需要带上了
每次里层的循环都会重新从头两两进行比对
每次只会对交换过的元素的后一个进行重新与下一个进行比较
比如 i与 i+1交换了, 那么下次会比较i+1与i+2, 而i不管换过来的与后续被交换来的大小如何, 都会放在下一轮比较中
该轮比较只会确定一个最值并一直向后移动, 不会管除最值之外的顺序, 那些会放在下一次进行比较
稳定排序, 若a = b, 原本在前的还会在前, 该算法是稳定的
最好情况, O(n), 已经正序了, 只会比较不会进行排序
最坏情况, O(n²), 即逆序, 全部都要比较并且交换
平均也就是冒泡排序的实际为O(n²)
代码
function bubbsort(nums) {
for(let i = 0; i < nums.length; i++){
for(let j = 0; j < nums.length - i; j++) {
let temp = arr[j]
if(arr[j] > arr[j + 1]) {
arr[j] = arr[j + 1]
arr[j + 1] = temp
}
}
}
}
选择排序
选择排序也是一个相对于暴力和直接的排序方法
从首位开始, 遍历一遍数组, 找到一个最小值, 记住索引, 随后交换首位和最小值位置
接着从第二位开始遍历数组, 找到最小值, 记住索引, 随后交换第二位和最小值位置
…
每次循环确定一个最小值放在前面, 直至遍历结束
由于是直接选择后插入到前面, 是不稳定的算法
最好最坏平均都是O(n²)
代码:
function selectSort(nums) {
for (let i = 0; i < nums.length; i++) {
let min = nums[i]
let index = i
for (let j = i + 1; j < nums.length; j++) {
if (nums[j] < min) {
min = nums[j]
index = j
}
}
let temp = nums[index]
nums[index] = nums[i]
nums[i] = temp
}
}
插入排序
插入排序不同于选择排序那样那么暴力的插入
而是插入到有序序列中的合适位置
何为有序序列, 即循环过程中构建出的
默认第一个是有序序列, 之后的元素会不断与前面的进行比较, 若小于比较的元素, 则前面的元素需要后移
不断后移, 直到到达一个不小于该元素的后方, 将该元素插入
此时后面的元素因为是不断后移的, 因此还是有序的
稳定的排序算法, 得益于比较的过程, 只会移动比自己大的元素到自己的后面
最好的情况O(n)
最坏的情况O(n²)
function insertSort(nums) {
for (let i = 1; i < nums.length; i++) {
let temp = nums[i]
let j = i - 1
while (j >= 0 && nums[j] > temp) {
nums[j + 1] = nums[j]
j --
}
nums[j + 1] = temp
}
}
希尔排序
希尔排序, 是对插入排序的优化
因为插入排序每次都需要移动大量元素, 希尔排序是对数组进行了分组, 组内进行插入排序
逐渐递减步长, 最后当步长为1时, 即每个元素都是一组, 但是元素已经基本有序了
组内插入排序不需要每次都移动大量元素, 这就是优化的地方
不稳定排序, 由于分组的步长原因, 会导致不稳定
最好情况 O(nlog2 n)
最坏情况 O(nlog2 n)
平均情况 O(nlog n)
代码:
function shellSort(nums) {
// 每一次大循环是以不同的步长为分界, 直至循环到步长为0
let step = Math.floor(nums.length / 2)
for (step; step > 0; step = Math.floor(step / 2)) {
for(let j = step; j < nums.length; j++){ // j相当于从每一组的第二个元素开始, 向后找, 与前一组的元素做比较
let temp = nums[j]
let k = j - step // 与前一组的元素做比较, 组内插入排序
while(k >=0 && nums[k] > temp){
nums[k + step] = nums[k]
k -= step
}
nums[k + step] = temp
}
}
}
归并排序
归并排序采用的是分治的思想, 即将一个数组分成许多小数组, 再给小数组继续分, 然后进行有序合并
最终得到的为有序数组
下面以二路分治为例, 即分为两路进行归并
采用递归的方法, 大数组向小数组要解
小数组向小小组要解
最底层为两路长度都为1的数组, 这个时候进行合并, 调用合并的方法
合并的方法会放一个结果数组, 将两路数组按照大小进行放入
最终两路最小数组合并结束, 为一个有序数组, 向上返回, 上层栈继续调用合并方法将这两个有序数组合并为一个大的有序数组
最终到最顶层栈, 两路合并返回一个有序数组
细节部分见下方注释
稳定排序, 因为一直是相邻两路之间对比
牺牲空间换取时间, 牺牲了递归的空间, 将时间复杂度由n * n换成了n * log n
代码:
function mergeSort(nums) {
let len = nums.length
if (len < 2) {// 分割为最小时, 递归结束, 向上返回
return nums
}
let mid = Math.floor(len / 2)
// merge接收两个参数, 分别为两路数组的一路, 然后进行有序合并, 并将该有序数组向上返回给上层栈调用
return merge(mergeSort(nums.slice(0, mid)), mergeSort(nums.slice(mid, len)))
}
function merge(arr1, arr2) {
let res = []
let i = 0, j = 0
while (i < arr1.length && j < arr2.length) {
// 将两路数组之间进行有序添加到结果数组中, 谁小就添加谁, 并且指针后移, 可能会有任意一个先遍历结束
if (arr1[i] < arr2[j]) {
res.push(arr1[i])
i++
} else {
res.push(arr2[j])
j++
}
}
// 两个while只会执行一次, 因为前面两个数组必然会有一个结束了, 并且两个数组均为有序数组
// 这时候只需要将未结束的都添加到结果数组中即可
while(i < arr1.length) {
res.push(arr1[i])
i++
}
while(j < arr2.length){
res.push(arr2[j])
j++
}
return res
}
快速排序
快速排序依旧采用的是分治的思想
选一个基准值(根据存储的数据结构, 以数组首为例), 比基准值小的排在其左边, 反之在右边, 中间是基准值
同时再对左右两边递归调用上述的方法
递归的出口是左右两边没有值的时候, 即只有一个值的时候
这样从最底层栈返回的一个值向上与中间值合并为 左中右 有序数组结构
继续向上返回 左中右 有序数组结构
顶层栈即为有序数组
不稳定排序, 因为与基准值比较移动造成的
牺牲递归的空间换取时间, 时间复杂度为 n * log n
代码:
function quickSort(arr) {
if (arr.length < 2) return arr
let base = arr[0]
let left = arr.filter((val, index) => {
return val <= base && index !== 0
})
let right = arr.filter((val, index) => {
return val > base && index !== 0
})
return [...quickSort(left), base, ...quickSort(right)]
}
堆排序
堆排序, 以大顶堆为例(正序数组, 排序结束后最大的在最后)
用数组来模拟一个堆, 数组下标 * 2 + 1为左节点, +2为右节点
大顶堆的父节点大于两个子节点
维护堆也就是建立堆所需要的方法见下面注释
在排序函数中首先由最后一个叶子节点的父亲开始循环调用维护堆的方法创建一个堆
随后从最后一个节点开始, 每次都和顶部节点(最大的值)交换, 最大的值放在了最后, 此时长度 - 1
调用维护堆的方法,传入一个长度, 维护除排好序外的堆
循环调用顶部和维护堆直至只剩一个元素
不稳定算法, 时间复杂度 n * log n
代码:
function adjust(arr, len, index) {
// 注意需要这个len长度, 因为维护一个堆的时候, 不一定是维护全部, 在后续排序时会有已经排好序的, 因此长度需要改变
/*
维护堆的函数, 向下递归比较, 不用管上层的, 上层的会在自身调用时向下比较
需要一个变量保存最大索引, 如果还是自身就返回
不是自身的话, 目标和索引位置调换
此时索引位置是原目标, 利用该索引继续递归向下比较, 因为目标传来后可能会比下面的小
*/
let end = -1
if (index * 2 + 1 >= len) return
// 找到最大的那个的索引
if (arr[index * 2 + 1] > arr[index * 2 + 2]) end = index * 2 + 1
else end = index * 2 + 2
if (arr[index] > arr[end]) return
let temp = arr[index]
arr[index] = arr[end]
arr[end] = temp
adjust(arr, len, end)
}
function heapSort(arr) {
/*
先建堆, 从最后一个叶子节点的父节点开始, 并非是一层一层的, 因为上述的维护堆的函数, 是直接比较自己和自己的左右孩子
*/
for(let i = Math.floor(arr.length/2) - 1; i >= 0; i--) {
adjust(arr, arr.length, i)
}
/*
建完堆以后, 堆顶元素是一个最大值, 将其与最尾部交换, 作为排好序的一员
此时最尾部的上到了第一个, 维护一下堆, 但是长度此时需要减小1, 因为已经排好一个了
*/
for(let i = arr.length - 1; i > 0; i--) {
let temp = arr[0]
arr[0] = arr[i]
arr[i] = temp
adjust(arr, i - 1, 0)
}
}
计数排序
计数排序
利用数组的索引天然有序的性质, 不需要比较就可以进行排序
将原数组中的值放在一个临时数组中, 原数组的值对应着临时数组的下标, 临时数组的值就是原数组中值出现的次数
这样一次遍历结束后, 临时数组中, 下标 = 原数组值的项不为0, 并且数组下标是有序的, 遍历时直接取值不为0的下标即为有序的值
临时数组下标项的值表示的是原数组中值=该下标的元素出现了几次, 因此遇见不为0的下标后, 需要循环一次将多个值都取出来
具体细节见下面函数的注释
不适用负数和小数, 因为没有负数和小数下标, 且最大值和最小值之间不能相差太大, 因为临时数组是利用最大值为长度创建的一个数组
用最大值创建的原因就是在于临时数组的下标值表示的是原数组的值
线性时间复杂度: kn, 在一定范围内速度最快, 不需要比较, 利用数组索引来存放值
浪费空间, 需要额外的数组, 利用索引存放原数组的值
上面提到很多次利用临时数组的索引 = 原数组的值, 反复强调便于理解
function countSort(arr) {
let max = Math.max(...arr) // 找出最大值, 用于临时数组的最大索引
let countArr = new Array(max + 1) // + 1是因为最大值对应的项为countArr[max]
countArr.fill(0)
for(let item of arr) {
countArr[item]++ // 不需要比较, 直接原数组值对应的临时数组的索引++即可
}
let index = 0 // 指向原数组的指针
for(let i = 0; i < countArr.length; i++){
// 遍历临时数组, 遇见不为0的项, 表示该项的索引, 即原数组的值出现了, 直接添加到原数组中即可
// 因为临时数组的索引(表示原数组的值)是有序的, 遇见了就添加即可
while(countArr[i] > 0) {
arr[index++] = i
countArr[i]--
}
}
return arr
}
桶排序
和上述计数排序一样的思想, 计数排序可以看做是特殊的桶排序, 即一个数一个桶
而泛化一点就是, 一个范围的数一个桶, 桶间由于是按照数值范围划分桶的, 因此桶间有序
桶内使用任意排序方法排序即可, 因为桶内数已经在一定范围内了
找一个最大值一个最小值, 和一个基准值, 例如2-102, 基准值为5, 那么(max - min) / 5 = 20, 表示最大的桶的索引为20
则需要创建长度为 20 + 1的桶
每个桶都是一个数组, 用来存放属于该桶范围内的数据, 存放完之后对该桶内排序
最好情况是数据均匀分布在每个桶里
最坏是数据全都在一个桶里, 直接退化
空间复杂度为n
时间复杂度和是否稳定需要考虑桶内使用的排序算法
function bucketSort(arr, count){
let max = arr[0]
let min = arr[0]
let DEAULTCOUNT = 5
for(let i = 1; i < arr.length; i++) {
max = Math.max(max, arr[i])
min = Math.min(min, arr[i])
}
count = count || DEAULTCOUNT // 基准值, 表示一个桶的存放范围
let size = Math.floor((max - min) / count) // 得到最大桶的索引
let bucket = new Array(size + 1) // 需要创建最大索引 + 1长度
for(let i = 0; i < bucket.length; i++) {
bucket[i] = []
}
for(let item of arr) {
let index = Math.floor((item - min) / count) // 举例, 2 - 102中, 6位于第几个桶, 首先要减去桶范围的起始值才能除以基准值, 如果直接除基准值, 那6就位于第二个桶了, 实际上即使第一个桶按照范围刚好到6才算第一个桶
bucket[index].push(item) // 找到对应桶的索引后, 将其放入
}
let index = 0 // 原数组的指针
for(let i = 0; i < bucket.length; i++) {
bucket[i] = quickSort(bucket[i]) // 桶内排序, 调用上文的快速排序算法
for(let j = 0; j < bucket[i].length; j++) {
arr[index++] = bucket[i][j]
}
}
}