归并排序、快排、堆排序的比较


个人理解,不同见解可以一起讨论。

在日常排序算法中,小数据量下,用啥区别都不大,但是数据量起来后,性能差异就会很大了。

而且在常用的大数据量的排序算法中,主要就是归并、快排和堆排,下面从几个方面一起看看这几种排序算法的异同。

算法比较

复杂度

  • 归并排序,时间复杂度是 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

posted on   进击的davis  阅读(80)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
历史上的今天:
2023-03-23 go 标准包flag的基本用法
2021-03-23 python uuid的连接及简单应用

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
点击右上角即可分享
微信分享提示