快速排序在数组有序和无序情况下的时间复杂度

本文探讨了快速排序在数组有序和无序情况下,采用不同分区算法和基数选择时的时间复杂度。简单分区和双指针分区算法在数组有序时可能导致最坏情况的时间复杂度O(n^2),而随机选择基数能保持平均时间复杂度O(nlogn)。在实际测试中,数组逆序和随机无序时,随机选择基数的效率优于固定选择首位元素。

快速排序有两点要注意:
一、partition分区算法的选择:

  1. 简单分区算法:选择一个基数,遍历数组元素和基数比较,如果元素nums[left]比基数大,就和数组nums[right]交换,right–,如果元素nums[left]比基数小,left++
  2. 双指针分区算法:从left和right分别定义一个指针,如果元素nums[left]比基数大,暂停遍历,如果元素nums[left]比基数小,left++;如果元素nums[right]比基数小,暂停遍历,如果元素nums[right]比基数大,right–;两边都暂停后互相交换元素,之后继续遍历,直到left和right指针相遇
    二、基数的选择:随意选择区间内任何一个数字做基数都可以。有两种选择方式:
  3. 选择第一个元素或者最后一个元素作为基数
  4. 选择区间内一个随机元素作为基数
    第二种选择方式的平均时间复杂度是最优的,一般为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)
	}
}

测试结果:

  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
  1. 数组元素逆序
➜  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
  1. 数组元素随机无序
➜  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

从数据可以得出的结论:

  1. 在数组有序的情况下无论是简单分区还是双指针分区算法,随机选取基数的算法效率都是固定基数选择第一位的算法效率的上百倍(O(n^2)/O(nlogn),n为10000);而在简单分区算法固定基数选择中,数组顺序情况下比较逆序情况下还是快了不少,原因应该是顺序较逆序少了元素的交换。
  2. 在数组元素随机无序下,无论是简单分区还是双指针分区算法,随机选取基数和固定基数选择的算法效率没有差别,但都比数组有序情况下随机选取基数的算法效率差一点,因为数组有序情况下随机选取基数的算法基数两边会完全平分数组,时间复杂度达到最优O(nlogn)。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值