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


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

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

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

算法比较

复杂度

  • 归并排序,时间复杂度是 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 2024-03-23 18:04  进击的davis  阅读(11)  评论(0编辑  收藏  举报

导航