前言
本文介绍了两部分算法实现:1) 寻找数组中第K大元素的快速选择算法;2) 数组排序的快速排序算法。
对于第K大元素问题,采用基于荷兰国旗分区的快速选择算法,通过随机选择基准值并三路分区(小于、等于、大于基准值),将平均时间复杂度优化至O(n)。算法首先将问题转换为查找第(n-k)小的元素,然后递归处理包含目标索引的分区。
排序算法部分代码未完整展示,但应基于类似的快速排序思想。两种算法都利用了分区操作,快速选择只需处理目标所在分区,而快速排序需要处理所有分区。
两种方法都采用随机化基准选择来避免最坏情况,空间复杂度为O(logn)。荷兰国旗分区特别适合处理含重复元素的数组。
数组中的第K个最大元素
代码实现
python
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
"""
使用荷兰国旗随机快排算法找到数组中第k个最大的元素
Args:
nums: 整数数组
k: 要找的第k个最大元素的位置
Returns:
数组中第k个最大的元素
"""
# 将问题转换为找第k小的元素(从右边数第k个,即从左边数第(n-k+1)个)
# 例如,对于[1,2,3,4,5],第2大的元素是4,对应第4小的元素
n = len(nums)
target_index = n - k # 转换为索引位置
# # 复制数组以避免修改原始数组
# arr = nums[:]
return self.quickSelect(nums, 0, n - 1, target_index)
def quickSelect(self, nums: List[int], left: int, right: int, k: int) -> int:
"""
快速选择算法,基于荷兰国旗分区
Args:
nums: 待处理数组
left: 左边界
right: 右边界
k: 目标索引
Returns:
第k个元素的值
"""
if left == right:
return nums[left]
# 随机选择pivot以避免最坏情况
random_index = random.randint(left, right)
# 如果保持pivot在原位置(不交换):
# 需要修改分区函数接口,传入pivot的具体索引
# 分区函数逻辑会更复杂,需要记住pivot原始位置
# 交换元素时需要特殊处理pivot位置
# 例:lt, gt = threeWayPartition(nums, left, right, pivot_index=2) # 需要额外参数
nums[random_index], nums[right] = nums[right], nums[random_index]
# 使用荷兰国旗分区
lt, gt = self.threeWayPartition(nums, left, right)
if k < lt:
# 目标在左半部分
return self.quickSelect(nums, left, lt - 1, k)
elif k > gt:
# 目标在右半部分
return self.quickSelect(nums, gt + 1, right, k)
else:
# 目标在中间部分,即等于pivot的区域
return nums[k]
def threeWayPartition(self, nums: List[int], left: int, right: int) -> tuple:
"""
荷兰国旗分区算法,将数组分为三部分:小于pivot、等于pivot、大于pivot
Args:
nums: 待分区数组
left: 左边界
right: 右边界
Returns:
(lt, gt) 其中lt是等于区的左边界,gt是等于区的右边界
"""
pivot = nums[right] # 选择最右边的元素作为pivot
lt = left # nums[left...lt-1] < pivot
gt = right # nums[gt+1...right] > pivot
i = left # nums[lt...i-1] == pivot
while i <= gt:
if nums[i] < pivot:
# 当前元素小于pivot,交换到左边
nums[lt], nums[i] = nums[i], nums[lt]
lt += 1
i += 1
elif nums[i] > pivot:
# 当前元素大于pivot,交换到右边
nums[i], nums[gt] = nums[gt], nums[i]
gt -= 1
# 注意:这里i不增加,因为从右边交换过来的元素还未检查
else:
# 当前元素等于pivot
i += 1
return lt, gt
算法实现说明
1. 主函数 findKthLargest :
- 将问题转换为找第k小的元素(从右边数第k个,即从左边数第(n-k+1)个)
- 例如,对于[1,2,3,4,5],第2大的元素是4,对应第4小的元素
- 使用 target_index = n - k 将问题转换为找目标索引位置的元素
2. 快速选择函数 quickSelect :
- 实现快速选择算法,基于荷兰国旗分区
- 随机选择pivot以避免最坏情况
- 根据分区结果决定在哪个子数组中继续查找
3. 荷兰国旗分区函数 threeWayPartition :
- 将数组分为三部分:小于pivot、等于pivot、大于pivot
- 使用三个指针 lt 、 i 、 gt 来维护分区状态
- 这种分区方法对于有重复元素的数组特别有效
执行流程
输入 : [3,2,1,5,6,4], k = 2
目标 : 找到数组中第2大的元素
步骤1 : 确定目标索引
-
数组长度n = 6
-
目标索引 = n - k = 6 - 2 = 4
-
我们要找排序后索引为4的元素(在降序排列中是第2大的元素)
步骤2 : 第一次快速选择
-
在整个数组[3,2,1,5,6,4]中选择随机元素作为基准值
-
假设选择了索引3的元素5,将其与最右边的元素交换
-
数组变成[3,2,1,4,6,5],基准值是5
步骤3 : 荷兰国旗分区
初始数组: [3,2,1,4,6,5],基准值pivot=5
荷兰国旗分区使用三个指针:
-
lt (less than): 小于pivot的区域右边界(lt左边都是小于pivot的元素)
-
gt (greater than): 大于pivot的区域左边界(gt右边都是大于pivot的元素)
-
i: 当前处理的元素位置
初始状态 :
-
lt = 0 (小于5的区域从索引0开始)
-
gt = 5 (大于5的区域从索引5开始)
-
i = 0 (从索引0开始处理)
-
数组: [3,2,1,4,6,5]
循环处理 :
第1次 : i=0, 元素=3
-
3 < 5,所以3应该放到小于pivot的区域
-
将索引0的元素3与lt位置(0)的元素交换(实际没变)
-
lt前移: lt=1, i前移: i=1
-
状态: [3,2,1,4,6,5], lt=1, i=1, gt=5
-
小于5区域: [3] (索引0)
第2次 : i=1, 元素=2
-
2 < 5,2应该放到小于pivot的区域
-
将索引1的元素2与lt位置(1)的元素交换(实际没变)
-
lt前移: lt=2, i前移: i=2
-
状态: [3,2,1,4,6,5], lt=2, i=2, gt=5
-
小于5区域: [3,2] (索引0-1)
第3次 : i=2, 元素=1
-
1 < 5,1应该放到小于pivot的区域
-
将索引2的元素1与lt位置(2)的元素交换(实际没变)
-
lt前移: lt=3, i前移: i=3
-
状态: [3,2,1,4,6,5], lt=3, i=3, gt=5
-
小于5区域: [3,2,1] (索引0-2)
第4次 : i=3, 元素=4
-
4 < 5,4应该放到小于pivot的区域
-
将索引3的元素4与lt位置(3)的元素交换(实际没变)
-
lt前移: lt=4, i前移: i=4
-
状态: [3,2,1,4,6,5], lt=4, i=4, gt=5
-
小于5区域: [3,2,1,4] (索引0-3)
第5次 : i=4, 元素=6
-
6 > 5,6应该放到大于pivot的区域
-
将索引4的元素6与gt位置(5)的元素交换
-
数组变成: [3,2,1,4,5,6]
-
gt前移: gt=4(注意i不变,因为从右边交换来的5还未检查)
-
状态: [3,2,1,4,5,6], lt=4, i=4, gt=4
-
大于5区域: [6] (索引5)
第6次 : i=4, 元素=5
-
5 == 5,5应该在等于pivot的区域
-
i前移: i=5
-
状态: [3,2,1,4,5,6], lt=4, i=5, gt=4
循环结束 : i(5) > gt(4),退出循环
最终分区结果 :
-
数组: [3,2,1,4,5,6]
-
lt = 4, gt = 4
-
小于5的区域: [3,2,1,4] (索引0到3)
-
等于5的区域: [5] (索引4到4)
-
大于5的区域: [6] (索引5到5)
步骤4 : 判断目标位置
-
目标索引是4
-
lt=4, gt=4
-
由于lt ≤ 目标索引 ≤ gt(即4 ≤ 4 ≤ 4),目标就在等于基准值的区域中
-
直接返回索引4处的元素:5
结果 : 第2大的元素是5
验证 : 原数组排序后为[1,2,3,4,5,6],第2大元素(降序第2位)确实是5
算法只经过一次分区就找到了答案,因为目标索引正好落在等于基准值的区域内,这是荷兰国旗分区算法的高效之处。
时间复杂度分析
- 平均时间复杂度:O(n),每次分区后只需要处理一个子数组
- 最坏时间复杂度:O(n²),但通过随机选择pivot可以避免大多数最坏情况
- 空间复杂度:O(log n),递归调用栈的深度
算法优势
- 不需要完全排序整个数组,只需找到目标元素
- 荷兰国旗分区能有效处理重复元素
- 随机化pivot选择避免最坏情况
- 平均情况下时间复杂度为O(n)
排序数组
代码实现
python
class Solution:
def sortArray(self, nums: List[int]) -> List[int]:
"""
使用荷兰国旗随机快排算法对数组进行升序排序
Args:
nums: 待排序的整数数组
Returns:
升序排列后的数组
"""
if not nums or len(nums) <= 1:
return nums
self.quickSort(nums, 0, len(nums) - 1)
return nums
def quickSort(self, nums: List[int], left: int, right: int):
"""
快速排序算法,基于荷兰国旗分区
Args:
nums: 待排序数组
left: 左边界
right: 右边界
"""
if left >= right:
return
# 随机选择pivot以避免最坏情况
random_index = random.randint(left, right)
nums[random_index], nums[right] = nums[right], nums[random_index]
# 使用荷兰国旗分区
lt, gt = self.threeWayPartition(nums, left, right)
# 递归排序小于pivot的部分
self.quickSort(nums, left, lt - 1)
# 递归排序大于pivot的部分
self.quickSort(nums, gt + 1, right)
# 等于pivot的部分已经在正确位置,无需排序
def threeWayPartition(self, nums: List[int], left: int, right: int) -> tuple:
"""
荷兰国旗分区算法,将数组分为三部分:小于pivot、等于pivot、大于pivot
Args:
nums: 待分区数组
left: 左边界
right: 右边界
Returns:
(lt, gt) 其中lt是等于区的左边界,gt是等于区的右边界
"""
pivot = nums[right] # 选择最右边的元素作为pivot
lt = left # nums[left...lt-1] < pivot
gt = right # nums[gt+1...right] > pivot
i = left # nums[lt...i-1] == pivot
while i <= gt:
if nums[i] < pivot:
# 当前元素小于pivot,交换到左边
nums[lt], nums[i] = nums[i], nums[lt]
lt += 1
i += 1
elif nums[i] > pivot:
# 当前元素大于pivot,交换到右边
nums[i], nums[gt] = nums[gt], nums[i]
gt -= 1
# 注意:这里i不增加,因为从右边交换过来的元素还未检查
else:
# 当前元素等于pivot
i += 1
return lt, gt
复杂度分析:
-
时间复杂度 :
- 平均情况:O(n log n)
- 最坏情况:O(n²),但通过随机选择pivot可以避免大多数最坏情况
- 比快速选择算法(O(n))要慢,因为需要对整个数组进行排序,而不是只找第k个元素
-
空间复杂度 :
- O(log n),主要是递归调用栈的空间