快速排序

1. 快速排序

1.1 基本思想

是冒泡排序的一种改进,核心思想是分治,对于子问题的主要思想是:选定一个基准元素,通过一趟排序将一个集合(数组)分割以基准元素为界为两部分,基准元素左边所有数据都比基准元素要小,基准元素右边所有数据都比基准元素要大,再对基准元素分割的两部分分别进行快速排序。

1.1.1 算法步骤:

  1. 在数组中,选择一个元素为基准(pivot)
  2. 将所有小于基准的元素移动到基准的左边,所有大于基准的元素移动到基准的右边(这个操作被称之为分区),分区结束后,基准元素的位置就是其最终排序位置
  3. 对基准元素左右分区重复执行第1和第2步,直至所有子集只剩一个元素

核心难点: 如何选择基准元素(主要影响时间复杂度)?如何执行单趟排序?

1.1.2 单趟排序(子问题)策略

以数组 A = [ 4 , 7 , 6 , 5 , 3 , 2 , 8 , 1 ] A = [4, 7, 6, 5, 3, 2, 8, 1] A=[4,7,6,5,3,2,8,1]为例

  1. 挖坑法
    挖坑法的思想是:设置一个基准元素 P i v o t Pivot Pivot(假设以第一个元素为基准, P i v o t = A [ 0 ] = 4 Pivot = A[0] = 4 Pivot=A[0]=4),分别设置一个左指针为 L e f t Left Left,赋予其初始值为数组起始位置:0,右指针为 R i g h t Right Right,赋予其初始值为数组末尾: l e n g t h ( A ) − 1 = 7 length(A) - 1 = 7 length(A)1=7,假定以 n u l l null null表示坑位,移动左右指针,挖下不满足条件位置的值,填入满足条件的值,直到两指针相遇,结束排序,具体操作如下:
    首先在基准元素位置挖下一个坑
    当前状态为:
    A = [ n u l l , 7 , 6 , 5 , 3 , 2 , 8 , 1 ] , L e f t = 0 , R i g h t = 7 A = [null, 7, 6, 5, 3, 2, 8, 1], Left = 0, Right = 7 A=[null,7,6,5,3,2,8,1],Left=0,Right=7
    A [ L e f t ] = A [ 0 ] = 4 ≤ P i v o t A[Left] = A[0] = 4 \leq Pivot A[Left]=A[0]=4Pivot,不动
    A [ R i g h t ] = A [ 7 ] = 1 < P i v o t A[Right] = A[7] = 1< Pivot A[Right]=A[7]=1<Pivot,将 A [ R i g h t ] A[Right] A[Right]的值挖出(形成一个新坑),填入坑原来的坑中, L e f t Left Left指针右移一位
    ⇒ \Rightarrow 状态更新为
    A = [ 1 , 7 , 6 , 5 , 3 , 2 , 8 , n u l l ] , L e f t = 1 , R i g h t = 7 A = [1, 7, 6, 5, 3, 2, 8, null], Left = 1, Right = 7 A=[1,7,6,5,3,2,8,null],Left=1,Right=7
    A [ L e f t ] = A [ 1 ] = 7 > P i v o t A[Left] = A[1] = 7 > Pivot A[Left]=A[1]=7>Pivot,将 A [ L e f t ] A[Left] A[Left]的值挖出(形成一个新坑),填入坑原来的坑中, R i g h t Right Right指针左移一位
    ⇒ \Rightarrow 状态更新为
    A = [ 1 , n u l l , 6 , 5 , 3 , 2 , 8 , 7 ] , L e f t = 1 , R i g h t = 6 A = [1, null, 6, 5, 3, 2, 8, 7], Left = 1, Right = 6 A=[1,null,6,5,3,2,8,7],Left=1,Right=6
    A [ R i g t h ] = A [ 6 ] = 8 > P i v o t A[Rigth] = A[6] = 8 > Pivot A[Rigth]=A[6]=8>Pivot,不挖坑, R i g h t Right Right指针左移一位
    ⇒ \Rightarrow 状态更新为
    A = [ 1 , n u l l , 6 , 5 , 3 , 2 , 8 , 7 ] , L e f t = 1 , R i g h t = 5 A = [1, null, 6, 5, 3, 2, 8, 7], Left = 1, Right = 5 A=[1,null,6,5,3,2,8,7],Left=1,Right=5
    A [ R i g t h ] = A [ 5 ] = 2 < P i v o t A[Rigth] = A[5]= 2 < Pivot A[Rigth]=A[5]=2<Pivot,将 A [ R i g h t ] A[Right] A[Right]的值挖出(形成一个新坑),填入坑原来的坑中, L e f t Left Left指针右移一位
    ⇒ \Rightarrow 状态更新为
    A = [ 1 , 2 , 6 , 5 , 3 , n u l l , 8 , 7 ] , L e f t = 2 , R i g h t = 5 A = [1, 2, 6, 5, 3, null, 8, 7], Left = 2, Right = 5 A=[1,2,6,5,3,null,8,7],Left=2,Right=5
    A [ L e f t ] = A [ 2 ] = 6 > P i v o t A[Left] = A[2] = 6 >Pivot A[Left]=A[2]=6>Pivot,将 A [ L e f t ] A[Left] A[Left]的值挖出(形成一个新坑),填入坑原来的坑中, R i g h t Right Right指针左移一位
    ⇒ \Rightarrow 状态更新为
    A = [ 1 , 2 , n u l l , 5 , 3 , 6 , 8 , 7 ] , L e f t = 2 , R i g h t = 4 A = [1, 2, null, 5, 3, 6, 8, 7], Left = 2, Right = 4 A=[1,2,null,5,3,6,8,7],Left=2,Right=4
    A [ R i g h t ] = A [ 4 ] = 3 < P i v o t A[Right] = A[4] = 3 < Pivot A[Right]=A[4]=3<Pivot,将 A [ R i g h t ] A[Right] A[Right]的值挖出(形成一个新坑),填入坑原来的坑中, L e f t Left Left指针右移一位
    ⇒ \Rightarrow 状态更新为
    A = [ 1 , 2 , 3 , 5 , n u l l , 6 , 8 , 7 ] , L e f t = 3 , R i g h t = 4 A = [1, 2, 3, 5, null, 6, 8, 7], Left = 3, Right = 4 A=[1,2,3,5,null,6,8,7],Left=3,Right=4
    A [ L e f t ] = A [ 3 ] = 5 > P i v o t A[Left] = A[3] = 5 >Pivot A[Left]=A[3]=5>Pivot,将 A [ L e f t ] A[Left] A[Left]的值挖出(形成一个新坑),填入坑原来的坑中, R i g h t Right Right指针左移一位
    ⇒ \Rightarrow 状态更新为
    A = [ 1 , 2 , 3 , n u l l , 5 , 6 , 8 , 7 ] , L e f t = 3 , R i g h t = 3 A = [1, 2, 3, null, 5, 6, 8, 7], Left = 3, Right = 3 A=[1,2,3,null,5,6,8,7],Left=3,Right=3
    L e f t Left Left R i g h t Right Right指针相遇,将 P i v o t Pivot Pivot的值填入坑中,即: A [ L e f t ] = P i v o t A[Left] = Pivot A[Left]=Pivot,排序结束
    ⇒ \Rightarrow 最终状态更新为
    A = [ 1 , 2 , 3 , 4 , 5 , 6 , 8 , 7 ] , L e f t = 3 , R i g h t = 3 A = [1, 2, 3, 4, 5, 6, 8, 7], Left = 3, Right = 3 A=[1,2,3,4,5,6,8,7],Left=3,Right=3
    附上这部分的硬核代码(后续给出优化)
#设置初始数组
A  = [4, 7, 6, 5, 3, 2, 8, 1]
#初始化:
Pivot = A[0] #选取第一个元素作为基准
Left = 0 #左指针设定为数据起始位置
Right = len(A) - 1 #右指针设定为数组末尾
A[0] = 'null' #在基准元素位置挖下第一个坑
while Left < Right:
    print(A) #方便观察数组排序的变化
    if type(A[Right]) != str: #每次挖完坑,更新的是另一头的指针,下一次就要从更新的指针位置开始动
        if A[Right] <= Pivot: #判断右边的值比基准元素小
            A[Left] = A[Right] #将该值放入先前挖好的左边坑里
            A[Right] = 'null' #被挖走的值形成新的坑
            Left += 1 #左边的坑被填充,左指针向右移动一步
        else: #如果右边的值比基准元素大
            Right -= 1 #将右指针向左移动一步
    
    if type(A[Left]) != str:
        if  A[Left] > Pivot: #判断左边的值比基准元素大
            A[Right] = A[Left] #将该值放入先前挖好的右边坑里
            A[Left] = 'null' #被挖走的值形成新的坑
            Right -= 1 #将右指针向左移动一步
        else: #如果右边的值比基准元素小
            Left += 1 #左指针向右移动一步
A[Left] = Pivot #左右指针相遇,跳出循环,将基准元素的值赋给左指针指向最终位置
  1. 指针交换法
    指针交换法的思想是:设定一个基本元素 P i v o t Pivot Pivot(这里仍然假设以第一个元素为基准, P i v o t = A [ 0 ] = 4 Pivot = A[0] = 4 Pivot=A[0]=4,分别设置一个左指针为 L e f t Left Left,赋予其初始值为数组起始位置:0,右指针为 R i g h t Right Right,赋予其初始值为数组末尾: l e n g t h ( A ) − 1 = 7 length(A) - 1 = 7 length(A)1=7,移动左右指针,使得左右指针所指元素同时不满足条件: A [ L e f t ] ≤ P i v o t , A [ R i g h t ] > P i v o t A[Left] \leq Pivot, A[Right] > Pivot A[Left]Pivot,A[Right]>Pivot,交换左右指针所指元素的值,继续移动指针,直到两指针相遇,结束排序,具体操作如下:
    初始状态为
    A = [ 4 , 7 , 6 , 5 , 3 , 2 , 8 , 1 ] , P o v i t = 4 , L e f t = 0 , R i g h t = 7 A = [4, 7, 6, 5, 3, 2, 8, 1], Povit = 4, Left = 0, Right = 7 A=[4,7,6,5,3,2,8,1]Povit=4,Left=0,Right=7
    → \rightarrow 开始寻找左指针:
    A [ L e f t ] = A [ 0 ] = 4 ≤ P i v o t A[Left] = A[0] = 4 \leq Pivot A[Left]=A[0]=4Pivot L e f t Left Left指针右移一位, L e f t = 1 Left = 1 Left=1
    A [ L e f t ] = A [ 1 ] = 7 > P i v o t A[Left] = A[1] = 7 > Pivot A[Left]=A[1]=7>Pivot,不满足条件,记录下左指针位置 L e f t = 1 Left = 1 Left=1
    → \rightarrow 开始寻找右指针:
    A [ R i g h t ] = A [ 7 ] = 1 < P i v o t A[Right] = A[7] = 1 < Pivot A[Right]=A[7]=1<Pivot,不满足条件,记录下右指针位置 R i g h t = 7 Right = 7 Right=7
    → \rightarrow 交换左右指针对应位置的值: t = A [ L e f t ] , A [ L e f t ] = A [ R i g h t ] , A [ R i g h t ] = t t = A[Left] , A[Left] = A[Right], A[Right] = t t=A[Left],A[Left]=A[Right],A[Right]=t L e f t Left Left指针向右移一位, R i g h t Right Right指针向左移一位
    ⇒ \Rightarrow 状态更新为
    A = [ 4 , 1 , 6 , 5 , 3 , 2 , 8 , 7 ] , P o v i t = 4 , L e f t = 2 , R i g h t = 6 A = [4, 1, 6, 5, 3, 2, 8, 7], Povit = 4, Left = 2, Right = 6 A=[4,1,6,5,3,2,8,7]Povit=4,Left=2,Right=6
    → \rightarrow 开始寻找左指针:
    A [ L e f t ] = A [ 2 ] = 6 > P i v o t A[Left] = A[2] = 6 > Pivot A[Left]=A[2]=6>Pivot,不满足条件,记录下左指针位置 L e f t = 2 Left = 2 Left=2
    → \rightarrow 开始寻找右指针:
    A [ R i g h t ] = A [ 6 ] = 8 > P i v o t A[Right] = A[6] = 8 > Pivot A[Right]=A[6]=8>Pivot R i g h t Right Right指针向左移一位, R i g h t = 5 Right = 5 Right=5
    A [ R i g h t ] = A [ 5 ] = 2 < P i v o t A[Right] = A[5] = 2 < Pivot A[Right]=A[5]=2<Pivot,不满足条件,记录下右指针位置 R i g h t = 5 Right = 5 Right=5
    → \rightarrow 交换左右指针对应位置的值: t = A [ L e f t ] , A [ L e f t ] = A [ R i g h t ] , A [ R i g h t ] = t t = A[Left] , A[Left] = A[Right], A[Right] = t t=A[Left],A[Left]=A[Right],A[Right]=t L e f t Left Left指针向右移一位, R i g h t Right Right指针向左移一位
    ⇒ \Rightarrow 状态更新为
    A = [ 4 , 1 , 2 , 5 , 3 , 6 , 8 , 7 ] , P o v i t = 4 , L e f t = 3 , R i g h t = 4 A = [4, 1, 2, 5, 3, 6, 8, 7], Povit = 4, Left = 3, Right = 4 A=[4,1,2,5,3,6,8,7]Povit=4,Left=3,Right=4
    → \rightarrow 开始寻找左指针:
    A [ L e f t ] = A [ 3 ] = 5 > P i v o t A[Left] = A[3] = 5 > Pivot A[Left]=A[3]=5>Pivot,不满足条件,记录下左指针位置 L e f t = 3 Left = 3 Left=3
    → \rightarrow 开始寻找右指针:
    A [ R i g h t ] = A [ 4 ] = 3 < P i v o t A[Right] = A[4] = 3 < Pivot A[Right]=A[4]=3<Pivot,不满足条件,记录下右指针位置 R i g h t = 4 Right = 4 Right=4
    → \rightarrow 交换左右指针对应位置的值: t = A [ L e f t ] , A [ L e f t ] = A [ R i g h t ] , A [ R i g h t ] = t t = A[Left] , A[Left] = A[Right], A[Right] = t t=A[Left],A[Left]=A[Right],A[Right]=t L e f t Left Left指针向右移一位, R i g h t Right Right指针向左移一位,但这时候我们发现,两个指针相遇,将 P i v o t Pivot Pivot所在的值与 A [ R i g h t ] A[Right] A[Right]的值交换, A [ 0 ] = A [ R i g h t ] , A [ R i g h t ] = P i v o t A[0] = A[Right], A[Right] = Pivot A[0]=A[Right],A[Right]=Pivot,排序结束
    ⇒ \Rightarrow 状态更新为
    A = [ 3 , 1 , 2 , 4 , 5 , 6 , 8 , 7 ] , P o v i t = 4 , L e f t = 4 , R i g h t = 3 A = [3, 1, 2, 4, 5, 6, 8, 7], Povit = 4, Left = 4, Right = 3 A=[3,1,2,4,5,6,8,7]Povit=4,Left=4,Right=3
    附上这部分的硬核代码(后续给出优化)
#初始化
A  = [4, 7, 6, 5, 3, 2, 8, 1]
Pivot = A[0] #选定基准元素
Left = 0 #左指针赋值为数组开始元素
Right = len(A) - 1 #右指针赋值为数组末端

while Left < Right:
    if A[Left] <= Pivot: #从左指针开始寻找
        Left += 1 #满足左边小于基准元素,左指针向右移动一位,直到不满足条件,停下记录指针位置
    if A[Right] > Pivot: #右指针寻找
        Right -= 1 #满足右边大于基准元素,右指针向左移动一位,直到不满足条件,停下记录指针位置
    if A[Left] > Pivot and A[Right] < Pivot: #如果左右两端指针都不满足基准元素条件
        A[Left], A[Right] = A[Right], A[Left] #交换左右指针所指位置的元素值
        Left += 1 #同时将左指针向右移动一位
        Right -= 1 #将右指针向左移动一位
    if Left >= Right: #两指针相遇
        A[0], A[Right] = A[Right], A[0] #交换基准元素与右指针所指元素的值,排序结束

挖坑法优化代码

def quicksort(data, start, end):
    Left = start #设置左指针位置
    Right = end #设置右指针位置
    pivot = data[start] #设置基准元素
    if Left >= Right: #可以看作是排序结束条件或者递归出口
        return data
    while Left < Right:
        while Left < Right and data[Right] >= pivot: #从右指针寻找第一个小于基准元素pivot的值
            Right -= 1
        data[Left] = data[Right] #找到后填入最开始挖的坑中,这里省略了挖的步骤,直接覆盖数据
        while Left < Right and data[Left] < pivot: #从左指针开始寻找第一个大于基准元素的值
            Left += 1
        data[Right] = data[Left] #填入右边挖好的坑中,同样省略了挖的步骤
    data[Left] = pivot #最后两个指针相遇,剩了最后一个坑,将基准元素填入
    quicksort(data, start, Left - 1) #对基准元素左边部分执行快速排序
    quicksort(data, Left + 1, end) #对基准元素右边部分执行快速排序

指针交换法优化代码

def quicksort(data, start, end):
    Left = start #设置左指针位置
    Right = end #设置右指针位置
    pivot = data[start] #设置基准元素
    if Left >= Right:#可以看作是排序结束条件或者递归出口
        return data
    while Left < Right:
        while Left < Right and data[Right] >= pivot:#寻找右边第一个小于基准元素pivot的值的位置
            Right -= 1
        while Left < Right and data[Left] < pivot:#寻找左边第一个大于基准元素pivot的值的位置
            Left += 1
        data[Left], data[Right] = data[Right], data[Left] #交换左右两个指针所指元素的值
    data[Left], data[data.index(pivot)] = data[data.index(pivot)], data[Left] #左右指针相遇,交换基准元素与左指针最终位置对应元素的值
    quicksort(data, start, Left - 1)#对基准元素左边部分执行快速排序
    quicksort(data, Left + 1, end)#对基准元素右边部分执行快速排序

1.1.3 Leetcode问题举例

  1. Leetcode第215题:数组中的第K个最大元素
    题目描述如下:在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
    说明: 你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。

示例1
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例2
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        '''
        快排每一趟排序之后一定能确定一个选定基准元素的位置,我们就看每一趟快排完成之后,
        基准元素返回的位置position是不是第k个,
        如果是,则返回,
        如果不是,观察返回的位置与k的大小关
        如果position < k,往右寻找,如果position > k,往左寻找
        '''
        self.k = len(nums) - k #顺数第k - 1个, n - k + (k - 1) = n - 1 = len(nums)
        return self.result(nums, 0, len(nums) - 1)
        
    def quicksort(self, data, start, end):
    	'''
    	这里面使用了挖坑法,详细的参考前文
    	'''
        left, right = start, end
        pivot = data[left]
        while left < right:
            while left < right and data[right] >= pivot:
                right -= 1
            data[left] = data[right]
            while left < right and data[left] < pivot:
                left += 1
            data[right] = data[left]
        data[left] = pivot
        return left
    
    def result(self, data, start, end):
        if start == end: #指针相遇,结束算法
            return data[start]
        position = self.quicksort(data, start, end)
        if position == self.k: #返回的基准元素下标等于题目想要寻找的k,返回该基准元素下标对应的值
            return data[position]
        if position < self.k: #基准元素下标比目标k小,向右寻找
            return self.result(data, position + 1, end)
        else:#基准元素下标比目标k大,向左寻找
            return self.result(data, start, position - 1)

Python语言内置排序函数,可以一行代码搞定,但是不建议使用这种玄学。

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        return sorted(nums)[len(nums) - k]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值