挑战全网代码量最短且易懂的快排 归并 堆排!

起因

写这篇文章的起因是看到网上有些写排序算法的代码写得比较冗长,而且不易懂,反正我有些看不懂,正好最近也在复习排序,于是coding!!

介绍

第一篇关于排序的算法都是选择时间复杂度为nlogn的,原因是实现较为复杂,日常或者面试中出现频率也比较高,感觉比较有意义。接下来我们来做一个简单介绍。

排序方法 时间复杂度(平均) 时间复杂度(最坏) 时间复杂度(最好) 空间复杂度
快速排序 O(n*log(n)) O(n^2) O(n*log(n)) O(log(n))
归并排序 O(n*log(n)) O(n*log(n)) O(n*log(n)) O(n)
堆排序 O(n*log(n)) O(n*log(n)) O(n*log(n)) O(1)

这只是一个大概的情况,因算法的实现不同可能空间复杂度会有差异,甚至写一个快排但其实是n^2复杂度的也不是不可能!接下来我们来具体实现吧

实现

快排

  • 基本思想 : 标记一个基准(priot)通过递归将比基准大的数放一边,比基准小的数放一边,递归的返回值是一个局部有序的序列

  • 算法描述

    • 从数组中挑选一个priot
    • 循环数组 将小于priot的元素放在left数组 将大于priot的元素放在right数组
    • 递归left,right数组 递归的条件是数组大小 >= 2
    • left, right递归返回的局部有序数组重组为更大的有序数组
  • gif图解

七行极简快排

js 复制代码
const quickSort = array => {
	if (array.length < 2) return array
	const priot = array.pop()
	const left = array.filter(num => num < priot) //left, right可以用reduce实现
	const right = array.filter(num => num >= priot)
	return [...quickSort(left), priot, ...quickSort(right)]
}

利用partition函数

js 复制代码
function quickSort(arr) {
	if (arr.length < 2) return arr
	const pivotIndex = partition(arr, 0, arr.length - 1)
	return [...quickSort(arr.slice(0, pivotIndex)), arr[pivotIndex], ...quickSort(arr.slice(pivotIndex + 1))]
}
function partition(arr, left, right) {
	const pivot = arr[right]
	let i = left - 1
	for (let j = left; j < right; j++) {
		if (arr[j] < pivot) {
			;[arr[++i], arr[j]] = [arr[j], arr[i]]
		}
	}
	;[arr[i + 1], arr[right]] = [arr[right], arr[i + 1]]
	return i + 1
}

归并排序

  • 算法思想 : 基于分治思想的排序算法,将无序数组大小不断切分,再不断数组有序化
  • 算法描述
    • 选择无序数组的中间索引,将数据切割为left, right数组
    • 递归left, right数组 临界值是数组大小 >= 2
    • 完成递归之后就开始合并无序数组
      • 新建临时数组arr, 储存合并之后的有序数组
      • 按升序或者降序的逻辑比较每个left, right数组中的元素,填入临时数组中
      • left, right数组中可能还有未比较的元素,直接连接到临时数组中 原因是他们都是有序化的数组,未比较的元素一定最大(升序)或者最小(降序)的,可以无缝连接arr
  • gif图解

十八行极简归并

js 复制代码
const mergeSort = array => {
	let length = array.length
	if (length < 2) return array
	const mid = Math.floor(length / 2)
	const left = array.slice(0, mid)
	const right = array.slice(mid)
	return merge(mergeSort(left), mergeSort(right))
}
const merge = (left, right) => {
	let arr = []
	let i = 0, j = 0;
	while (i < left.length && j < right.length) {
		if (left[i] > right[j]) arr.push(right[j++])
        else arr.push(left[i++])
	}
	arr = arr.concat(left.slice(i), right.slice(j))
	return arr
}

堆排序

  • 算法思想 :类似于完全二叉树的结构 将待排序数组视为一个二叉堆,然后通过一系列的堆调整操作,使得数组满足大根堆(或小根堆)
  • 算法描述
    • heapify函数
      • 是堆调整函数,旨在将当前节点cur调整到属于它的位置
      • 参数array, cur, end分别是代调整数组,当前节点, 无序堆最末位置
      • 根据大根堆(或小根堆)的逻辑更新当前节点cur是否比其左右节点大(小), 如果cur更新则交换元素 并继续向下调整
    • 建堆 从最后一个非叶子节点调整
    • 形成有序堆 每次把堆顶和最后一个元素交换,有序序列从最后一个元素开始增加 并重新调整堆 最后整体有序
  • gif图解

十九行极简堆排

js 复制代码
const heapSort = array => {
	const length = array.length
	for (let i = Math.floor(length / 2) - 1; i >= 0; i--) heapify(array, i, length)
	for (let i = length - 1; i > 0; i--) {
		[array[0], array[i]] = [array[i], array[0]]
		heapify(array, 0, i)
	}
	return array
}
const heapify = (array, cur, end) => {
	let max = cur
	let left = cur * 2 + 1, right = cur * 2 + 2
	if (left < end && array[left] > array[max]) max = left
	if (right < end && array[right] > array[max]) max = right
	if (max !== cur) {
		[array[max], array[cur]] = [array[cur], array[max]]
		heapify(array, max, end)
	}
}

测试效率

虽然看起来实现了都是nlogn的时间复杂度的算法,但是不免怀疑真的比我的冒泡排序,选择排序快吗?? 实践出真知! coding!

写一个测试时间的函数

  • genRandomList 生成10000个随机数的数组
  • testCostTime 计算消耗的时间 isOrder并且判断是否是有序的 result.slice切片前20个数看看效果
js 复制代码
// testTime.js
const genRandomList = () => {
	const array = Array.from({ length: 10000 }, (_, index) => index)
	for (let i = 0; i < array.length; i++) {
		let randomIndex = Math.floor(Math.random() * (i + 1))
		;[array[randomIndex], array[i]] = [array[i], array[randomIndex]]
	}
	return array
}
const testCostTime = (array, sort, name) => {
	const start = performance.now()
	let result = sort(array)
	let isOrder = result?.every((value, index, array) => index == 0 || value >= array[index])
	console.log(`${name} 耗时 ${performance.now() - start} ${isOrder ? '排序正确' : '排序错误'} ${result?.slice(0, 20)}`)
}
export { genRandomList, testCostTime }

Promise.all实现异步同时测试

  • genRandomList统一生成的array增加公平性
  • testAlgorithmTime封装一层Promise来异步调用
  • Promise.all包裹测试的promise
js 复制代码
//index.js
import quickSort from './quick.js'
import mergeSort from './merge.js'
import heapSort from './heap.js'
import bubbleSort from './bubble.js'
import { testCostTime, genRandomList } from './testTime.js'

const array = genRandomList()
const testAlgorithmTime = (array, algorithm, name) => {
	return new Promise(resolve => {
		testCostTime(array, algorithm, name)
		resolve()
	})
}
Promise.all([
	testAlgorithmTime(array, quickSort, 'quickSort'),
	testAlgorithmTime(array, heapSort, 'heapSort'),
	testAlgorithmTime(array, mergeSort, 'mergeSort'),
	testAlgorithmTime(array, bubbleSort, 'bubbleSort'),
])
	.then(e => {
		console.log('测试完成')
	})
	.catch(error => {
		console.error('出现错误:', error)
	})

测试结果 🎉🎉🎉

符合预期 手工! ✨

参考

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom11 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom11 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试