排序之快速排序|Go主题月

870 阅读3分钟

快速排序简述

归并排序一样,快速排序也采用分治策略,但不需要使用额外的存储空间。不过,算法的效率会有所下降。

  1. 快速排序算法首先选出一个基准值。为简单起见,我们选取切片数组(下面简称列表)中的第一个元素。
  2. 基准值的作用是帮助切分列表
  3. 在最终的有序列表中,基准值的位置被称作分割点,算法在分割点切分列表为两部分,然后对这两部分应用快速排序。

快速排序过程

假设需要对列表[5, 3, 10, 1]应用快速排序算法进行升序排序,过程如下图:

  1. 选取基准值:元素 5 将作为第一个基准值。

快速排序基准值.png 2. 计算分割点(为基准值找到正确的位置)而进行分区操作: 它会找到分割点,同时将其他元素放到正确的一边——分割点的左边的数据都小于基准值;分割点右边的数据都大于基准值。

分区操作首先找到两个坐标——leftmark 和 rightmark——它们分别位于列表剩余元素的开头和末尾,如下图所示。
分区的目的是根据待排序元素与基准值的相对大小将它们放到正确的一边,同时逐渐逼近分割点。
下图展示了为基准值元素 5 寻找正确位置的过程。

快速排序图解.png

首先加大 leftmark,直到遇到一个大于基准值的元素。然后减小 rightmark,直到遇到一个小于基准值的元素。这样一来,就找到两个与最终的分割点错序的元素。
本例中,这两个元素就是 10 和 1。互换这两个元素的位置,然后重复上述过程。

当 rightmark 小于 leftmark 时,过程终止。此时,rightmark 的位置就是分割点。将基准值与当前位于分割点的元素互换,即可使基准值位于正确位置,如上图所示。
分割点左边的所有元素都小于基准值,右边的所有元素都大于基准值。因此,可以在分割点处将列表一分为二,并针对左右两部分递归调用快速排序函数。

Golang实现快速排序算法代码

1. package main
2. 
3. import "fmt"
4. 
5. func quickSort(numList []int) {
6. 	quickSortHelper(numList, 0, len(numList)-1)
7. }
8. 
9. func quickSortHelper(numList []int, first, last int) {
10. 	if first < last {
11. 		// 计算切割点
12. 		splitPoint := partition(numList, first, last)
13. 		// 对切割点左边的数据进行快速排序
14. 		// 即对 比切割点都小的数据进行快速排序
15. 		quickSortHelper(numList, first, splitPoint-1)
16. 		// 对切割点右边的数据进行快速排序
17. 		// 即对 比切割点都大的数据进行快速排序
18. 		quickSortHelper(numList, splitPoint+1, last)
19. 	}
20. }
21. 
22. func partition(numList []int, first, last int) int {
23. 	// 选择第一个数作为基准点
24. 	// 把列表中的比基准点小的数据放在基准点的左边
25. 	// 把列表中的比基准点大的数据放在基准点的右边
26. 	// 并且返回切割点所在的位置,以便再次进行切割
27. 	pivotValue := numList[first]
28. 	leftMark := first + 1
29. 	rightMark := last
30. 	done := false
31. 	// 寻找切割点
32. 	for !(done) {
33. 		// 当左浮标从左向右移动的过程中
34. 		// 且左右浮标没有交叉的时候
35. 		// 如果左浮标的值比基准值小,则继续往右移动
36. 		// 如果左浮标的值比基准值大,停止往右移动
37. 		// 意味着下一个值应该处于基准值的右边
38. 		for leftMark <= rightMark &&
39. 			numList[leftMark] <= pivotValue {
40. 			leftMark += 1
41. 		}
42. 
43. 		// 当右浮标从右向左移动的过程中
44. 		// 且左右浮标没有交叉的时候
45. 		// 如果右浮标的值比基准值大,则继续往左移动
46. 		// 如果右浮标的值比基准值小,停止向左移动
47. 		// 意味着这个值应该处于基准值的左边
48. 		for rightMark >= leftMark &&
49. 			numList[rightMark] >= pivotValue {
50. 			rightMark -= 1
51. 		}
52. 
53. 		// 如果左右浮标交叉,说明已找到切割点的位置
54. 		// 停止寻找切割掉
55. 		if rightMark < leftMark {
56. 			done = true
57. 		} else {
58. 			// 左右浮标没有交叉但是左右浮标都停下来了
59. 			// 即左右浮标的值应该处于基准值的另一边
60. 			// 那么需要互相交换左右浮标值
61. 			temp := numList[leftMark]
62. 			numList[leftMark] = numList[rightMark]
63. 			numList[rightMark] = temp
64. 		}
65. 	}
66. 
67. 	// 把切割点上的数据移动到基准值所在的位置
68. 	numList[first] = numList[rightMark]
69. 	// 把基准值移动到正确的位置上
70. 	numList[rightMark] = pivotValue
71. 	// 返回分割点
72. 	return rightMark
73. }
74. 
75. 
76. func main() {
77. 	numList := []int{10, 1, 5, 3, 99}
78. 	quickSort(numList)
79. 	fmt.Println(numList)
80. }

在上面代码中,快速排序函数 quickSort 调用了递归函数 quickSortHelper。

quickSortHelper 首先判断是否处于基本情况。

  1. 如果列表的长度小于或等于 1,说明它已经是有序列表;
  2. 如果长度大于 1,则进行分区操作并递归地排序。

时间复杂度

  1. 对于长度为 n 的列表,如果分区操作总是发生在列表的中部,就会切分 logn 次。
  2. 为了找到分割点,n 个元素都要与基准值比较。所以,时间复杂度是O(nlogn)。
    另外,快速排序算法不需要像归并排序算法那样使用额外的存储空间