1. 快速排序
1.1 基本思想
是冒泡排序的一种改进,核心思想是分治,对于子问题的主要思想是:选定一个基准元素,通过一趟排序将一个集合(数组)分割以基准元素为界为两部分,基准元素左边所有数据都比基准元素要小,基准元素右边所有数据都比基准元素要大,再对基准元素分割的两部分分别进行快速排序。
1.1.1 算法步骤:
- 在数组中,选择一个元素为基准(pivot)
- 将所有小于基准的元素移动到基准的左边,所有大于基准的元素移动到基准的右边(这个操作被称之为分区),分区结束后,基准元素的位置就是其最终排序位置
- 对基准元素左右分区重复执行第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]为例
- 挖坑法
挖坑法的思想是:设置一个基准元素 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]=4≤Pivot,不动
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 #左右指针相遇,跳出循环,将基准元素的值赋给左指针指向最终位置
- 指针交换法
指针交换法的思想是:设定一个基本元素 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]=4≤Pivot, 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问题举例
- 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]