归并排序、快排、堆排序的比较
个人理解,不同见解可以一起讨论。
在日常排序算法中,小数据量下,用啥区别都不大,但是数据量起来后,性能差异就会很大了。
而且在常用的大数据量的排序算法中,主要就是归并、快排和堆排,下面从几个方面一起看看这几种排序算法的异同。
算法比较
复杂度
-
归并排序,时间复杂度是 O(NLogN),空间复杂度 O(N),排序中如果用递归方式,可以认为排序分3步,分别是:切分数组,排序数组,合并数组,其中排序可以认为是 O(N),合并过程是 O(logN)(对半二分),所以时间复杂度就是 O(NlogN),而每次合并的时候都需要根据当次合并总长分配新的空间,是 O(N)。
-
快速排序,时间复杂度分情况,最好是 O(NlogN)(比如随机主元等),最坏 O(N^2)(有序数组,全部都要比较一次),空间复杂度 O(LogN),主要是递归
-
堆排序,时间复杂度,O(NlogN),空间复杂度 O(1)
稳定性
-
归并排序,作为一种外部排序算法(通过磁盘),是稳定的,即排序前后元素相等时相对位置不变
-
快排和堆排,都是内部排序算法(内存),不稳定
应用场景
- 小数据量是用插入也不错
- 数据量很大情况下,数据分布随机下,认为快排是优于另外两种排序算法的
- 对于topK问题,就是典型的用堆排很合适
- 链表排序时,适合用归并排序,外部排序时也是用归并好些
- 数据量大且相对有序时,快排性能弱化
算法实现
先看看几种排序算法的 golang 实现:
归并排序
package main func mergeSort(nums []int) []int { var sort func(nums []int) []int sort = func(nums []int) []int { if len(nums) <= 1 {return nums} mid := len(nums)/2 left := sort(nums[:mid]) right := sort(nums[mid:]) return merge(left, right) } return sort(nums) } func merge(left, right []int) []int { mergeNums := make([]int, len(left)+len(right)) var l, r, pos int for l < len(left) && r < len(right) { if left[l] < right[r] { mergeNums[pos] = left[l] l++ } else { mergeNums[pos] = right[r] r++ } pos++ } copy(mergeNums[pos:], left[l:]) copy(mergeNums[pos+len(left)-l:], right[r:]) return mergeNums }
快速排序
package main func quickSort(nums []int) []int { if len(nums) < 2 {return nums} var sort func(nums []int, left, right int) []int sort = func(nums []int, left, right int) []int { if left > right {return nil} i, j, pivot := left, right, nums[left] for i < j { for i < j && nums[j] >= pivot { j-- } for i < j && nums[i] <= pivot { i++ } nums[i], nums[j] = nums[j], nums[i] } nums[i], nums[left] = nums[left], nums[i] sort(nums, left, i-1) sort(nums, i+1, right) return nums } return sort(nums, 0, len(nums)-1) }
堆排序
package main func heapSort(nums []int) []int { if len(nums) <= 1 {return nums} var heapify func(root, end int) heapify = func(root, end int) { // 父节点小的值一直下沉 for { child := root * 2 + 1 if child > end { return } if child < end && nums[child] < nums[child+1] { child++ } // 节点已经满足 if nums[root] > nums[child] { return } // 父节点值通过交换实现下沉 nums[root], nums[child] = nums[child], nums[root] root = child } } // 堆化,逆序,非叶子节点 end := len(nums) - 1 for i := end/2; i >= 0; i-- { heapify(i, end) } // 通过交换尾值实现排序,逆序 for i := end; i >= 0; i-- { nums[i], nums[0] = nums[0], nums[i] end-- // 每次从root节点需要继续堆化 heapify(0, end) } return nums }
冒泡排序
package main func bubbleSort(nums []int) []int { if len(nums) < 2 {return nums} for i := 0; i < len(nums); i++ { for j := 0; j < len(nums)-i-1; j++ { if nums[j] > nums[j+1] { nums[j], nums[j+1] = nums[j+1], nums[j] } } } return nums }
测试
主要看看性能测试,分别构造 100、10w数据量下的排序,及随机或有序下的测试数据,我们直接看看运行情况吧。
数据量100
从结果中可看到,性能方面测试的数量级都差不多,区别不大。
cpu: AMD Ryzen 7 4700U with Radeon Graphics BenchmarkBubbleSort-8 73220 16343 ns/op 896 B/op 1 allocs/op BenchmarkBubbleSortWithSortedNums-8 64839 18364 ns/op 896 B/op 1 allocs/op BenchmarkHeapSort-8 110065 10721 ns/op 896 B/op 1 allocs/op BenchmarkHeapSortWithSortedNums-8 76599 15799 ns/op 896 B/op 1 allocs/op BenchmarkMergeSort-8 76648 15133 ns/op 6496 B/op 100 allocs/op BenchmarkMergeSortWithSortedNums-8 62953 19627 ns/op 6496 B/op 100 allocs/op BenchmarkQuickSort-8 108087 10960 ns/op 896 B/op 1 allocs/op BenchmarkQuickSortWithSortedNums-8 68344 17309 ns/op 896 B/op 1 allocs/op
数据量10w
从结果中可以看到,大数据量下的快排是比较好的,其次是堆排,然后是归并,当然大数据量下的冒泡就真的太慢了,已排序下没看到效果。
cpu: AMD Ryzen 7 4700U with Radeon Graphics BenchmarkBubbleSort-8 1 15113452100 ns/op 802816 B/op 1 allocs/op BenchmarkBubbleSortWithSortedNums-8 1 17634917200 ns/op 802816 B/op 1 allocs/op BenchmarkHeapSort-8 124 9622666 ns/op 802819 B/op 1 allocs/op BenchmarkHeapSortWithSortedNums-8 1 14242102500 ns/op 802816 B/op 1 allocs/op BenchmarkMergeSort-8 64 16613728 ns/op 14860611 B/op 100000 allocs/op BenchmarkMergeSortWithSortedNums-8 1 14223875600 ns/op 14860544 B/op 100000 allocs/op BenchmarkQuickSort-8 148 8062138 ns/op 802817 B/op 1 allocs/op BenchmarkQuickSortWithSortedNums-8 1 16200460500 ns/op 802816 B/op 1 allocs/op
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
2023-03-23 go 标准包flag的基本用法
2021-03-23 python uuid的连接及简单应用