LeetCode215. 数组中的第K个最大元素、LeetCode912. 排序数组

前言

本文介绍了两部分算法实现: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),递归调用栈的深度

算法优势

  1. 不需要完全排序整个数组,只需找到目标元素
  2. 荷兰国旗分区能有效处理重复元素
  3. 随机化pivot选择避免最坏情况
  4. 平均情况下时间复杂度为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

复杂度分析:

  1. 时间复杂度 :

    • 平均情况:O(n log n)
    • 最坏情况:O(n²),但通过随机选择pivot可以避免大多数最坏情况
    • 比快速选择算法(O(n))要慢,因为需要对整个数组进行排序,而不是只找第k个元素
  2. 空间复杂度 :

    • O(log n),主要是递归调用栈的空间
相关推荐
cqbzcsq2 小时前
蛋白质功能预测模型DAMPE论文阅读报告
论文阅读·人工智能·python·深度学习·生物信息学
转转技术团队2 小时前
回收团队基于Cursor集成MCP的智能代码修复提示词生成实践
人工智能·python·程序员
Einsail2 小时前
天梯赛题解(3-6)
算法
杜子不疼.2 小时前
【LeetCode 852 & 162_二分查找】山脉数组的峰顶索引 & 寻找峰值元素
算法·leetcode·职场和发展
山楂树の2 小时前
搜索插入位置(二分查找)
数据结构·算法
石头dhf2 小时前
大模型配置
开发语言·python
南科1号2 小时前
Tushare数据来源分析一例
python
helloyangkl2 小时前
Draco——参数说明
算法
API技术员3 小时前
京东API接口:如何高效获取商品详情与SKU信息
python