归并排序、快排、堆排序的比较
个人理解,不同见解可以一起讨论。
在日常排序算法中,小数据量下,用啥区别都不大,但是数据量起来后,性能差异就会很大了。
而且在常用的大数据量的排序算法中,主要就是归并、快排和堆排,下面从几个方面一起看看这几种排序算法的异同。
算法比较
复杂度
-
归并排序,时间复杂度是 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