众所周知,快速排序是基于划分的思想对一个数组进行排序,把一个数组分成三块。分别是:小于基准值的,等于基准值的,大于基准值的,然后再对小于和大于基准值的部分再进行划分,最后直到划分区间长度为 1 时停止。
整个快速排序的过程是递归的,因为对一个数组第一次划分完成之后还需要对剩下的两部分进行划分。通常代码可能写成这样:
func QuickSort(arr []int) {
if len(arr) < 2 || arr == nil {
return
}
quickSort(arr, 0, len(arr)-1)
}
func quickSort(arr []int, l, r int) {
if l < r {
mid := partition(arr, l, r)
quickSort(arr, l, mid-1)
quickSort(arr, mid+1, r)
}
}
// 将数组划分为三块,小于基准值、等于基准值、大于基准值
func partition(arr []int, l, r int) int {
less := l - 1
for i := l; i <= r; i++ {
if arr[i] <= arr[r] {
less++
arr[i], arr[less] = arr[less], arr[i]
}
}
return less
}
虽然这个算法的时间复杂度已经是 ,但是仔细观察你会发现,递归调用的时候是阻塞式的,下一个递归调用必须得等上一个递归调用完成才可以运行,这样就会损失一部分性能,而且这两个递归函数是彼此独立的谁也不依赖谁,可以独立计算,所以我们可以使用多协程来优化使它变成非阻塞的。
需要定义一个 WaitGroup
指针传递给处理函数。因为 Go 中的结构体是值传递的,在函数中必须要使用指针才可以操作同一个结构体。当然我们还需要控制递归的深度,就是并发规模,也不是每一次递归都需要创建协程,因为我们都知道不论是进程还是线程、协程它们上下文切换的时候都会产生一定的性能损耗,所以也不是创建的协程越多越好。
定义一个 maxDeep 来控制递归的深度。partition 函数不用变,最终代码可以写成这样:
func QuickSortConcurrent(arr []int) {
if len(arr) < 2 || arr == nil {
return
}
wg := &sync.WaitGroup{}
wg.Add(1)
go quickSortConcurrent(arr, 0, len(arr)-1, wg, 1)
wg.Wait()
}
const maxDeep = 9
func quickSortConcurrent(arr []int, l, r int, wg *sync.WaitGroup, dp int) {
if dp <= maxDeep+1 {
defer wg.Done()
}
if l < r {
mid := partition(arr, l, r)
if dp <= maxDeep {
wg.Add(2)
go quickSortConcurrent(arr, l, mid-1, wg, dp+1)
go quickSortConcurrent(arr, mid+1, r, wg, dp+1)
} else {
quickSortConcurrent(arr, l, mid-1, wg, dp+1)
quickSortConcurrent(arr, mid+1, r, wg, dp+1)
}
}
}
经过我的测试,当 maxDeep 设置为 9 的时候性能最优,也就是说我们创建了 个协程。
最终测试结果如下:
// 10.60s
func TestQuickSort(t *testing.T) {
var arr []int
max := 1_000_000_00
rand.Seed(time.Now().Unix())
for i := 0; i < max; i++ {
arr = append(arr, rand.Intn(max))
}
QuickSort(arr)
}
// 3.45s
func TestQuickSortConcurrent(t *testing.T) {
var arr []int
max := 1_000_000_00
rand.Seed(time.Now().Unix())
for i := 0; i < max; i++ {
arr = append(arr, rand.Intn(max))
}
QuickSortConcurrent(arr)
}
1 亿数据量,单协程版的用时 10.60s,多协程版的用时 3.45s,性能还是很可观的。