js排序的时间复杂度_JavaScript实现十大排序算法

一 : 冒泡排序

人们开始学习排序算法时,通常都先学冒泡算法,因为它在所有排序算法中最简单。然而, 从运行时间的角度来看,冒泡排序是最差的一个,接下来你会知晓原因

冒泡排序比较所有相邻的两个项,如果第一个比第二个大,则交换它们。元素项向上移动至 正确的顺序,就好像气泡升至表面一样,冒泡排序因此得名。

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

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值