经典排序算法及Go语言pqdsort的简单实现 | 青训营笔记

102 阅读4分钟

前言

本文的目标是带领读者领略经典排序算法,然后讲解pdqsort混合排序算法的设计思路,并尝试实现一个简单的Go语言版本的pdqsort算法。

本文代码仓库:github.com/shanliao420…

1. 经典排序算法

1.1 插入排序

insertionSort.gif 以上图片来自菜鸟教程-插入排序

算法描述

首先将第一个元素看作已排序数组,将后续的元素看作未排序数组,然后从前向后扫描,依次将元素插入对应的位置即可。

代码实现

func InsertionSort(nums []int) {
	n := len(nums)
	for i := 1; i < n; i++ {
		x := nums[i]
		j := i - 1
		for j >= 0 {
			if nums[j] >= x {
				nums[j+1] = nums[j]
				j--
			} else {
				break
			}
		}
		nums[j+1] = x
	}
}

复杂度分析

  1. 最好时间复杂度:O(n) | 在数组有序时取得

  2. 最坏时间复杂度:O(n^2) | 在数组逆序时取得

  3. 平均时间复杂度: O(n^2)

  4. 空间复杂度: O(1)

  5. 该排序算法稳定

1.2 归并排序

mergeSort.gif 以上图片来自菜鸟教程-归并排序

算法描述

将数组不断分为两段,此操作递归进行,将细分的数组段进行合并,因为同时有递归和合并的操作,因此叫归并排序。

代码实现

func MergeSort(nums []int, start int, end int) {
	if start == end {
		return
	}

	mid := (start + end) / 2
	MergeSort(nums, start, mid)
	MergeSort(nums, mid+1, end)

	mergeNums(nums, start, mid, end)
}

func mergeNums(nums []int, start, mid, end int) {
	lStart := start
	rStart := mid + 1
	sortedNum := make([]int, end-start+1)
	mergePointer := 0
	for lStart <= mid && rStart <= end {
		if nums[lStart] <= nums[rStart] {
			sortedNum[mergePointer] = nums[lStart]
			mergePointer++
			lStart++
		} else {
			sortedNum[mergePointer] = nums[rStart]
			mergePointer++
			rStart++
		}
	}
	for lStart <= mid {
		sortedNum[mergePointer] = nums[lStart]
		mergePointer++
		lStart++
	}

	for rStart <= end {
		sortedNum[mergePointer] = nums[rStart]
		mergePointer++
		rStart++
	}
	for i := start; i <= end; i++ {
		nums[i] = sortedNum[i-start]
	}
}

复杂度分析

  1. 时间复杂度: O(nlogn)

  2. 空间复杂度: O(n)

  3. 该排序算法稳定

1.3 快速排序

quickSort.gif 以上图片来自菜鸟教程-快速排序

算法描述

每次选定一个(随意选定)元素作为标兵,每次遍历操作都将数组分为比标兵元素小的一部分和比标兵元素大的一部分,然后递归的对这两部分分别快速排序。

代码实现

package SingleSort

func swap(a *int, b *int) {
	*a, *b = *b, *a
}

func swapMid(nums []int, left int, right int) {
	mid := (left + right) / 2
	if nums[left] > nums[mid] && nums[right] > nums[left] {
		mid = left
	}
	if nums[right] > nums[mid] && nums[left] > nums[right] {
		mid = right
	}
	swap(&nums[left], &nums[mid])
}

func partition(nums []int, left int, right int) int {
	swapMid(nums, left, right)
	pivot := left
	base := nums[pivot]
	for left < right {
		for left < right && nums[right] >= base {
			right--
		}
		for left < right && nums[left] <= base {
			left++
		}
		swap(&nums[left], &nums[right])
	}
	swap(&nums[left], &nums[pivot])
	return left
}

func partitionForVersion(nums []int, left int, right int) int {
	swapMid(nums, left, right)
	pivot := left
	minThan := left + 1
	for i := left + 1; i <= right; i++ {
		if nums[pivot] >= nums[i] {
			swap(&nums[minThan], &nums[i])
			minThan++
		}
	}
	swap(&nums[pivot], &nums[minThan-1])
	pivot = minThan - 1
	return pivot
}

func QuickSort(nums []int, left int, right int) {
	if left >= right {
		return
	}
	pivot := partition(nums, left, right)
	QuickSort(nums, left, pivot-1)
	QuickSort(nums, pivot+1, right)
}

复杂度分析

  1. 最坏情况时间复杂度: O(n^2) | 在数组有序时取得

  2. 最好情况时间复杂度: O(nlogn) | 每次选取的标兵正好是中位数时取得

  3. 平均时间复杂度: O(nlogn)

  4. 空间复杂度: O(1)

  5. 该排序算法不稳定

1.4 堆排序

heapSort.gif

以上图片来自菜鸟教程-堆排序

算法描述

堆排序通过构建堆的操作,和元素出堆的操作,实现排序。

堆的性质:子结点的键值总是小于(或者大于)它的父节点。

代码实现

func HeapSort(nums []int) {
	n := len(nums)
	for i := n - 1; i >= 0; i-- {
		shiftDown(nums, i, n)
	}
	for i := 0; i < n; i++ {
		swap(&nums[0], &nums[n-1-i])
		shiftDown(nums, 0, n-1-i)
	}
}

func shiftDown(nums []int, index int, n int) {
	for true {
		leftChild := index*2 + 1
		rightChild := index*2 + 2
		maxIndex := index
		if leftChild < n && nums[leftChild] > nums[maxIndex] {
			maxIndex = leftChild
		}
		if rightChild < n && nums[rightChild] > nums[maxIndex] {
			maxIndex = rightChild
		}
		if maxIndex == index {
			break
		}
		swap(&nums[maxIndex], &nums[index])
		index = maxIndex
	}
}

复杂度分析

  1. 时间复杂度: O(nlogn)

  2. 空间复杂度: O(1)

  3. 该排序算法不稳定

2. PDQSort设计思路

经典排序算法中,像插入排序对于有序数组,时间复杂度达到O(n), 此时快速排序时间复杂度O(n^2), 但是对于一般随机数组,插入排序时间复杂度达到O(n^2),此时使用快速排序将会比使用插入排序好得多,而对于堆排序而言,无论数组是何种情况,时间复杂度均为O(nlogn),当快速排序不理想时,可使用堆排序稳定时间复杂度。

数据量为128时各排序算法测试情况
goos: darwin
goarch: amd64
pkg: work.tangthinker/sort_algorithm/SingleSort
cpu: Intel(R) Core(TM) i5-4670T CPU @ 2.30GHz
BenchmarkHeapSortRandom
BenchmarkHeapSortRandom-4        	  108019	     10186 ns/op
BenchmarkQuickSortRandom
BenchmarkQuickSortRandom-4       	  139963	      8083 ns/op
BenchmarkInsertionSortRandom
BenchmarkInsertionSortRandom-4   	  143040	      7317 ns/op
BenchmarkHeapSortSorted
BenchmarkHeapSortSorted-4        	  355382	      3261 ns/op
BenchmarkQuickSortSorted
BenchmarkQuickSortSorted-4       	  887174	      1349 ns/op
BenchmarkInsertionSortSorted
BenchmarkInsertionSortSorted-4   	 4726950	       245.6 ns/op
PASS

Process finished with the exit code 0

PDQSort(pattern-defeating-quicksort) : 一种不稳定的混合排序算法。

  1. 在数据量不大时,使用插入排序

  2. 在数据量大时,使用快速排序

  3. 在快速排序效率不佳时,使用堆排序

  4. 效率不佳体现在好几次选取的pivot在数组的两端附近

3. PDQSort简单实现

package pdqSort

var (
	limit = 10
)

func PdqSort(nums []int, left int, right int) {
	if (right - left + 1) <= 24 {
		insertionSort(nums, left, right)
		return
	}
	if limit <= 0 {
		heapSort(nums, left, right)
		return
	}
	quickSort(nums, left, right)
}

func swap(a *int, b *int) {
	*a, *b = *b, *a
}

func getMid(nums []int, left int, right int) int {
	mid := (left + right) / 2
	condition := mid
	if nums[left] > nums[mid] && nums[right] > nums[left] {
		mid = left
	}
	if nums[right] > nums[mid] && nums[left] > nums[right] {
		mid = right
	}
	if condition == mid {
		return -1
	}
	return mid
}

func partition(nums []int, left int, right int) int {
	pivot := left
	base := nums[pivot]
	for left < right {
		for left < right && nums[right] >= base {
			right--
		}
		for left < right && nums[left] <= base {
			left++
		}
		swap(&nums[left], &nums[right])
	}
	swap(&nums[left], &nums[pivot])
	return left
}

func insertionSort(nums []int, start int, end int) {
	for i := start + 1; i < end; i++ {
		x := nums[i]
		j := i - 1
		for j >= 0 {
			if nums[j] >= x {
				nums[j+1] = nums[j]
				j--
			} else {
				break
			}
		}
		nums[j+1] = x
	}
}

func quickSort(nums []int, left int, right int) {
	if left >= right {
		return
	}
	mid := getMid(nums, left, right)
	if mid == -1 {
		insertionSort(nums, left, right)
		return
	}
	swap(&nums[left], &nums[mid])
	pivot := partition(nums, left, right)
	if pivot < (left+right)/8 {
		limit--
	}
	PdqSort(nums, left, pivot-1)
	PdqSort(nums, pivot+1, right)
}

func heapSort(nums []int, start int, end int) {
	n := end + 1
	for i := n - 1; i >= start; i-- {
		shiftDown(nums, i, n)
	}
	for i := start; i < n; i++ {
		swap(&nums[start], &nums[n-1-i])
		shiftDown(nums, start, n-1-i)
	}
}

func shiftDown(nums []int, index int, n int) {
	for true {
		leftChild := index*2 + 1
		rightChild := index*2 + 2
		maxIndex := index
		if leftChild < n && nums[leftChild] > nums[maxIndex] {
			maxIndex = leftChild
		}
		if rightChild < n && nums[rightChild] > nums[maxIndex] {
			maxIndex = rightChild
		}
		if maxIndex == index {
			break
		}
		swap(&nums[maxIndex], &nums[index])
		index = maxIndex
	}
}

总结

排序算法在数据量小时,其实无法体现时各自的优势,在数据量大时,方能体现各自的优势。在众多排序算法的武林中,分而治之的思想无处不在,且使用这种思想的算法一般比较效率高,本次总结的PDQSort混合排序算法,一是采用了分而治之的的思想,而是采取了模式选择的思想,因此能达到比较好的效率。