一 : 冒泡排序
人们开始学习排序算法时,通常都先学冒泡算法,因为它在所有排序算法中最简单。然而, 从运行时间的角度来看,冒泡排序是最差的一个,接下来你会知晓原因
冒泡排序比较所有相邻的两个项,如果第一个比第二个大,则交换它们。元素项向上移动至 正确的顺序,就好像气泡升至表面一样,冒泡排序因此得名。
export function bubbleSort(arr: number[]) {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
;[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
return arr
}
冒泡排序每一轮(外层循环) 会选出一个最小 或者最大的数组 放到数组最后
所以N 个 数字 只用 选N -1 次 =>外层循环 i
内存循环 两两相比, 只用 比较 N - 1次 ,但是 外层循环已经找到的最值不用比较
所以 => 内层循环 j < arr.length - i -1
一幅图来描述 冒泡的工作流程
二 :选择排序
选择排序算法是一种原址比较排序算法。 他解决了冒泡 交换次数过多的毛病,在冒泡排序中 需要交换 O(N^2) 次 但 选择排序中 只用交换 O(N)次
选择排序大致的思路是找到数据结构中的最小值并 将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。
export function selectionSort(arr: number[]) {
let min = 0
for (let i = 0; i < arr.length - 1; i++) {
min = i
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
min = j
}
}
if (i !== min) {
;[arr[i], arr[min]] = [arr[min], arr[i]]
}
}
return arr
}
选择排序的代码执行过程 如下图
三 :插入排序
插入排序的思想十分的重要, 学会了他你才能学习 希尔排序,而 希尔排序 又是排序算法 历史上的一个转折点 他打破了 排序算法 时间复杂度平均不会低于 O(N^2) 的理论。
插入排序每次排一个数组项,以此方式构建最后的排序数组。假定第一项已经排序了。接着, 它和第二项进行比较——第二项是应该待在原位还是插到第一项之前呢?这样,头两项就已正确 排序,接着和第三项比较(它是该插入到第一、第二还是第三的位置呢),以此类推
export function insertionSort(arr: number[]) {
for (let i = 1; i < arr.length; i++) {
let j = i
let temp = arr[i]
//插入操作 while (j > 0 && arr[j - 1] > temp) {
arr[j] = arr[j - 1]
j--
}
arr[j] = temp
}
return arr
}
插入排序的 过程 假设数组 [ 3 ,5 , 1, 4, 2]
一开始 把 3 当作局部有序 , 把 5 拎出来 和 局部有序 数列比较 并插入
// 第一次 : 5 > 3 合理 进入下一次循环
// 第二次 : 把 1 拎出来 插入 , 局部有序变为 : [ 3,5 ] 并比较 数组变为[1,3,5,4,2]
//第三次 : 把 4拎出来 插入 , 局部有序 变为 [1,3,5 ] 数组变为 [ 1,3,4,5, 2 ]
// 第四次 : 把2拎出来插入 局部有序 变为 [1,3,4,5 ] 数组变为 [ 1,2,3,4,5
//结束循环
四 : 希尔排序
希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”。
他是 第一个 将 排序算法 复杂度降低 到 O (N^2)之下的 . 但是他的复杂度 至今未被证明 猜测 效率 为 O (N^1.3)
他就是插入排序的一种进阶。只要你会了插入排序 你就能写出希尔排序
//复杂度为被证明,猜测 为 O (N ^1.3)export function shellSort(arr: number[]) {
//获取增量 let gap = Math.floor(arr.length / 2)
//增量等于1 即为 插入排序 原始。 一定会将数组排好 这个时候结束循环 while (gap >= 1) {
//进行插入排序 for (let i = gap; i < arr.length; i++) {
let j = i
let temp = arr[i]
while (j > gap - 1 && arr[j - gap] > temp) {
arr[j] = arr[j - gap]
j -= gap
}
arr[j] = temp
}
//缩小增量 gap = Math.floor(gap / 2)
}
return arr
}
注意: 插入排序 就是 进行 gap(增量) 为 1 时候的操作 ,
在希尔排序 中 初始增量 为数组长度一半 , 并依次缩小 .这是原版的做法
事实上 这个 增量 有别的计算方式,可以让 希尔排序 效率更高 接近O(N^4/5) 但是他不是我们学习 希尔排序 的重点 , 关于增量的证明 与改进 更多是数学方面的知识 我们基于面试 能够实现基本的希尔排序 就可以了 .
五 : 快速排序
快速排序也许是最常用的排序算法了。它的复杂度为 O(nlog(n)),且性能通常比其他复杂度 为 O(nlog(n))的排序算法要好。 快速排序 使用分而治之的方法 .
快速排序也是二十世纪 十大算法之一,感兴趣的朋友可以了解一下
我们重点学习 快速排序。
我们先看下阮一峰老师给出的快速排序算法:
function quickSort(arr) {
if (arr.length <= 1) return arr
let left = [],
right = []
//将中间值 取除 并在arr中移除 let middle = arr.splice(Math.floor(arr.length / 2), 1)[0]
arr.forEach((el) => (el >= middle ? right.push(el) : left.push(el)))
return quickSort1(left).concat(middle, quickSort1(right))
}
例如 数组 [ 5, 4 ,3, 2 ,1 ] 先取出 中间值 3 => [5,4,2,1]
然后 将数组一分为二 => 小于 3 的在左边 大于 3 的在右边
=> left [ 2 , 1 ] right[ 5, 4]
=>然后将 左右 数组 递归
=> [2,1] => middle :2 left[ 1 ] right [ ]
=> [5,4 ] => middle :5 left[ 4 ] right [ ]
=>最后 依次拼接数组 left+middle +right => [1,2,3,4,5]
注意: 上面的算法 使用了 splice 效率 非常的差,只是基于快速排序的思想, 写出的 比较容易理解的代码 我们来看下效率 更好 的快排 是如何做的
export function quickSort(arr: number[]) {
quick(arr, 0, arr.length - 1)
return arr
}
function quick(arr: number[], left: number, right: number) {
if (arr.length > 1) {
let index = partition(arr, left, right)
// 两半局部有序分开 递归排序 分而治之 if (left < index - 1) {
quick(arr, left, index - 1)
}
if (index < right) {
quick(arr, index, right)
}
}
}
function partition(arr: number[], left: number, right: number) {
//将数组 以pivot 为标准 划分为 两半 局部有序的 let pivot = arr[Math.floor((left + right) / 2)]
let i = left
let j = right
while (i <= j) {
while (arr[i] < pivot) {
i++
}
while (arr[j] > pivot) {
j--
}
if (i <= j) {
swap(arr, i, j)
i++
j--
}
}
return i
}
function swap(arr: number[], i: number, j: number) {
;[arr[i], arr[j]] = [arr[j], arr[i]]
}
这种实现思路 是利用 i j 两个指针 , 将 数组 以 pivot(枢纽) 分割成两半
左边 小于 pivot 右边大于pivot ,然后递归 分而治之 .
例如 数组 [ 3 , 6 , 5 , 7 , 4 , 1 , 8 , 2 , 9]
选取pivot : 4 , left : 3 right : 9
//left 会一直找到 >= 4 时停住
//right 会一直找到<= 4 停住
=> left =>6 , right => 2 这个时候 swap
[3,2,5,7,4,1,8,6,9] 继续循环
=>left =>5 , right =>1 调用 swap
[ 3,2,1,7,4,5,8,6,9]
=>left => 7 , right =>4 调用 swap
[3,2,1,4,7,5,8,6,9] 退出循环 ( i > j )
你会发现 在 4 的左边 都比 4 小
在 4 的右边都比 4 大 ,这个时候以4 为枢纽分开 递归即可
六 归并排序
归并排序也是一个可以实际使用的排序算法。 归并排序性能不错,其复杂度为 O(nlog(n))。
// JavaScript 的 Array 类定义了一个 sort 函数(Array.prototype.sort)用以 排序 JavaScript 数组(我们不必自己实现这个算法)。ECMAScript 没有定义用哪 个排序算法,所以浏览器厂商可以自行去实现算法。例如,Mozilla Firefox 使用归并排序作为 Array.prototype.sort 的实现,而 Chrome(V8 引擎)使用了 一个快速排序的变体
一张图 足以 说明 归并排序 的流程
export function mergeSort(arr: number[]) {
//分而治之 //先分 if (arr.length > 1) {
//将数组 分成两半 递归进行 直到 数组长度 小于等于 1 let middle = Math.floor(arr.length / 2)
let left = mergeSort(arr.slice(0, middle))
let right = mergeSort(arr.slice(middle, arr.length))
//然后 合并排序 arr = merge(left, right)
}
//将结果返回 return arr
}
function merge(left: number[], right: number[]) {
//将左右两个数组 合并 排序 //i 指向 左数组 j 指向右数组 let i = 0
let j = 0
//将结果有序的push 进 result 中 let result = []
while (i < left.length && j < right.length) {
//排序 result.push(left[i] < right[j] ? left[i++] : right[j++])
}
//合并 =>将 左右 数组 剩余的部分 concat return result.concat(i < left.length ? left.slice(i) : right.slice(j))
}
七 : 计数排序
计数排序是一个分布式排序 , 它是用来排序整数的优秀算法(它是一个整数排序算法),时间复杂度为 O(n+k),其中 k 是 临时计数数组的大小;但是,它确实需要更多的内存来存放临时数组。
这个排序 算法 的思路 就是 计数
=> 将每个数 当作索引 存进 数组(count)中,如果重复出现 则 索引对应的值 ++ ,
这个时候遍历我们的 的数组(count) ,他就排好序了。非常的简单
export function countingSort(arr: number[]) {
let count = [] //计数 let result = [] //结果 arr.forEach((el) => {
//将每个数字 以索引 存入 count中 if (!count[el]) {
count[el] = 0
}
count[el]++
})
count.forEach((el, i) => {
//将count 取出 if (el && el > 0) {
for (let j = 0; j < el; j++) {
result.push(i)
}
}
})
return result
}
你会发现 这个算法 只能存储 整数 且 需要消耗大量的空间, 但是在 有些时候利用此算法排序 非常的 好用 , 比如 按照年龄排序, 按照 成绩排序 .
即 量大 范围小(年龄 0 岁~ 150岁) 时 非常好用的算法 ,因为他的效率高 , 因为范围小 空间消耗也是 可以接受的。
八 : 基数排序
基数排序也是一个分布式排序算法,它是根据关键字排序的。
什么是关键字排序 ? 我们就按数组排序 为例 ,他先比较 每个数组的个位 按 计数排序处理, 再比较数组的 十位. 计数排序 处理 ..... 直到比较完最高位 就排好序了.
其实也就是 重复 进行我们的计数排序 ,来节省 空间的浪费
export function radixSort(arr: number[]) {
if (arr.length < 2) {
return arr
}
//找到最大值 let max = -Infinity
arr.forEach((el) => (el > max ? (max = el) : null))
//求他的位数 let digit = (max + '').length
//循环计数排序 let count = []
for (let i = 0; i < digit; i++) {
//按 个位排序, 十位排序 ,百位排序 .... arr.forEach((el) => {
let str = el + ''
let temp = +str[str.length - 1 - i]
if (isNaN(temp)) {
temp = 0
}
if (Array.isArray(count[temp])) {
count[temp].push(el)
} else {
count[temp] = [el]
}
})
arr = []
count.forEach((el) => {
if (Array.isArray(el)) {
el.forEach((e) => {
arr.push(e)
})
}
})
count = []
}
return arr
}
九 :堆排序
这个我在二叉堆里介绍过了。
十 : 桶排序
桶排序(也被称为箱排序)也是分布式排序算法,它将元素分为不同的桶(较小的数组), 再使用一个简单的排序算法,例如插入排序(用来排序小数组的不错的算法),来对每个桶进行 排序。然后,它将所有的桶合并为结果数组。
其实 上面的计数排序 基数排序 应该都属于 我们的桶排序,
但是桶排序 实际用的并不多 , 我们掌握 计数 和基数 两个变体 就好了.
感兴趣的可以去单独了解桶 排序 , 看看 他为什么 用的不多 。
十一 : 总结
然后我又基于 随机的数组 (十万个随机数)
,分别进行了上面的 排序算法 我们来看下他的耗时
你可以发现 冒泡 简直不能用, 等了24秒
最快 的计数排序 6 ms 就搞定了。。 但是耗费了大量的空间
官方自带的sort 在大部分情况下 速度还是慢于 我们的 快速排序
最后贴上单元测试。
import { createRandomArray } from '../util'
import { bubbleSort } from './01 冒泡排序'
import { selectionSort } from './02 选择排序'
import { insertionSort } from './03 插入排序'
import { shellSort } from './04 希尔排序'
import { quickSort } from './05 快速排序'
import { mergeSort } from './06 归并排序'
import { countingSort } from './07 计数排序'
import { radixSort } from './08 基数排序'
describe('排序算法测试', () => {
test('冒泡排序测试', () => {
const arr = createRandomArray(10)
const sortArr = Array.from(arr).sort((a, b) => a - b)
expect(bubbleSort(arr)).toEqual(sortArr)
})
test('选择排序测试', () => {
const arr = createRandomArray(10)
const sortArr = Array.from(arr).sort((a, b) => a - b)
expect(selectionSort(arr)).toEqual(sortArr)
})
test('插入排序测试', () => {
const arr = createRandomArray(10)
const sortArr = Array.from(arr).sort((a, b) => a - b)
expect(insertionSort(arr)).toEqual(sortArr)
})
test('希尔排序测试', () => {
const arr = createRandomArray(10)
const sortArr = Array.from(arr).sort((a, b) => a - b)
expect(shellSort(arr)).toEqual(sortArr)
})
test('快速排序', () => {
const arr = createRandomArray(10)
const sortArr = Array.from(arr).sort((a, b) => a - b)
expect(quickSort(arr)).toEqual(sortArr)
})
test('归并排序', () => {
const arr = createRandomArray(10)
const sortArr = Array.from(arr).sort((a, b) => a - b)
expect(mergeSort(arr)).toEqual(sortArr)
})
test('计数排序', () => {
const arr = createRandomArray(10)
const sortArr = Array.from(arr).sort((a, b) => a - b)
expect(countingSort(arr)).toEqual(sortArr)
})
test('基数排序', () => {
const arr = createRandomArray(10)
const sortArr = Array.from(arr).sort((a, b) => a - b)
expect(radixSort(arr)).toEqual(sortArr)
})
})
创建随机数组
'../util'
export function createRandomArray(size: number) {
const array = []
for (let i = 0; i < size; i++) {
array.push(Math.floor(Math.random() * 100))
}
return array
}