快速排序有两点要注意:
一、partition分区算法的选择:
- 简单分区算法:选择一个基数,遍历数组元素和基数比较,如果元素nums[left]比基数大,就和数组nums[right]交换,right–,如果元素nums[left]比基数小,left++
- 双指针分区算法:从left和right分别定义一个指针,如果元素nums[left]比基数大,暂停遍历,如果元素nums[left]比基数小,left++;如果元素nums[right]比基数小,暂停遍历,如果元素nums[right]比基数大,right–;两边都暂停后互相交换元素,之后继续遍历,直到left和right指针相遇
二、基数的选择:随意选择区间内任何一个数字做基数都可以。有两种选择方式: - 选择第一个元素或者最后一个元素作为基数
- 选择区间内一个随机元素作为基数
第二种选择方式的平均时间复杂度是最优的,一般为O(nlogn),第一种在数组基本有序的情况下容易造成一边倒的结果,算法效率不高,时间复杂度为O(n^2)
相关算法题:leetcode-912
func quickSort(nums []int,start,end int){
if start>=end{
return
}
mid:=partition(nums,start,end)
quickSort(nums,start,mid-1)
quickSort(nums,mid+1,end)
}
简单分区算法
使用数组第一个元素作为基数pivot(leetcode结果大概为70-90ms)
func partition(nums []int,start,end int)int{
pivot:=nums[start]//使用数组第一个元素作为基数
left:=start+1
right:=end
for left<right{
if nums[left]>pivot{
nums[left],nums[right]=nums[right],nums[left]
right--
}else{
left++
}
}
if nums[left]>pivot{
nums[left-1],nums[start]=nums[start],nums[left-1]
return left-1
}else{
nums[left],nums[start]=nums[start],nums[left]
return left
}
}
使用数组中间元素作为基数pivot(leetcode结果大概为40-50ms)
func partition(nums []int,start,end int)int{
pivot:=nums[start+(end-start)/2]
nums[start],nums[start+(end-start)/2]=nums[start+(end-start)/2],nums[start]
left:=start+1
right:=end
for left<right{
if nums[left]>pivot{
nums[left],nums[right]=nums[right],nums[left]
right--
}else{
left++
}
}
if nums[left]>pivot{
nums[left-1],nums[start]=nums[start],nums[left-1]
return left-1
}else{
nums[left],nums[start]=nums[start],nums[left]
return left
}
}
双指针分区算法
使用数组第一个元素作为基数pivot(leetcode结果大概为900-1000ms)
func partition(nums []int,start,end int)int{
pivot:=nums[start]
left:=start+1
right:=end
for left<right{
for left<right&&pivot>=nums[left]{
left++
}
for left<right&&pivot<nums[right]{
right--
}
if left<right{
nums[left],nums[right]=nums[right],nums[left]
left++
right--
}
}
if nums[left]>pivot{
nums[left-1],nums[start]=nums[start],nums[left-1]
return left-1
}else{
nums[left],nums[start]=nums[start],nums[left]
return left
}
}
使用数组中间元素作为基数pivot(leetcode结果大概为30-50ms)
func partition(nums []int,start,end int)int{
pivot:=nums[start+(end-start)/2]
nums[start],nums[start+(end-start)/2]=nums[start+(end-start)/2],nums[start]
left:=start+1
right:=end
for left<right{
for left<right&&pivot>=nums[left]{
left++
}
for left<right&&pivot<nums[right]{
right--
}
if left<right{
nums[left],nums[right]=nums[right],nums[left]
left++
right--
}
}
if nums[left]>pivot{
nums[left-1],nums[start]=nums[start],nums[left-1]
return left-1
}else{
nums[left],nums[start]=nums[start],nums[left]
return left
}
}
benchmark测试
创建10000个数组,每个数组10000个元素作为测试数据
package main
import (
"math/rand"
"testing"
)
func genNums() [][]int {
nums := make([][]int, 10000)
for i := 0; i < 10000; i++ {
nums[i] = make([]int, 10000)
for j := 0; j < 10000; j++ {
nums[i][j] = rand.Int()
}
}
return nums
}
func BenchmarkQuickSort(b *testing.B) {
nums := genNums()
b.ResetTimer()
for i := 0; i < b.N; i++ {
quickSort(nums[i], 0, len(nums[i])-1)
}
}
func BenchmarkQuickSorMid(b *testing.B) {
nums := genNums()
b.ResetTimer()
for i := 0; i < b.N; i++ {
quickSortMid(nums[i], 0, len(nums[i])-1)
}
}
func BenchmarkQuickSortDoublePointers(b *testing.B) {
nums := genNums()
b.ResetTimer()
for i := 0; i < b.N; i++ {
quickSortDoublePointer(nums[i], 0, len(nums[i])-1)
}
}
func BenchmarkQuickSortMidDoublePointers(b *testing.B) {
nums := genNums()
b.ResetTimer()
for i := 0; i < b.N; i++ {
quickSortMidDoublePointer(nums[i], 0, len(nums[i])-1)
}
}
测试结果:
- 数组元素顺序
➜ quicksort git:(master) ✗ go test -bench=. -benchtime=10000x .
goos: darwin
goarch: amd64
pkg: testhello/sort/quicksort
BenchmarkQuickSort-16 10000 1155496 ns/op
BenchmarkQuickSorMid-16 10000 160788 ns/op
BenchmarkQuickSortDoublePointers-16 10000 25391273 ns/op
BenchmarkQuickSortMidDoublePointers-16 10000 108620 ns/op
PASS
ok testhello/sort/quicksort 272.638s
- 数组元素逆序
➜ quicksort git:(master) ✗ go test -bench=. -benchtime=10000x .
goos: darwin
goarch: amd64
pkg: testhello/sort/quicksort
BenchmarkQuickSort-16 10000 21051258 ns/op
BenchmarkQuickSorMid-16 10000 140386 ns/op
BenchmarkQuickSortDoublePointers-16 10000 25137699 ns/op
BenchmarkQuickSortMidDoublePointers-16 10000 123618 ns/op
PASS
ok testhello/sort/quicksort 465.945s
- 数组元素随机无序
➜ quicksort git:(master) ✗ go test -bench=. -benchtime=10000x .
goos: darwin
goarch: amd64
pkg: testhello/sort/quicksort
BenchmarkQuickSort-16 10000 516184 ns/op
BenchmarkQuickSorMid-16 10000 538857 ns/op
BenchmarkQuickSortDoublePointers-16 10000 490184 ns/op
BenchmarkQuickSortMidDoublePointers-16 10000 507996 ns/op
PASS
ok testhello/sort/quicksort 35.857s
从数据可以得出的结论:
- 在数组有序的情况下无论是简单分区还是双指针分区算法,随机选取基数的算法效率都是固定基数选择第一位的算法效率的上百倍(O(n^2)/O(nlogn),n为10000);而在简单分区算法固定基数选择中,数组顺序情况下比较逆序情况下还是快了不少,原因应该是顺序较逆序少了元素的交换。
- 在数组元素随机无序下,无论是简单分区还是双指针分区算法,随机选取基数和固定基数选择的算法效率没有差别,但都比数组有序情况下随机选取基数的算法效率差一点,因为数组有序情况下随机选取基数的算法基数两边会完全平分数组,时间复杂度达到最优O(nlogn)。
本文探讨了快速排序在数组有序和无序情况下,采用不同分区算法和基数选择时的时间复杂度。简单分区和双指针分区算法在数组有序时可能导致最坏情况的时间复杂度O(n^2),而随机选择基数能保持平均时间复杂度O(nlogn)。在实际测试中,数组逆序和随机无序时,随机选择基数的效率优于固定选择首位元素。
630

被折叠的 条评论
为什么被折叠?



