最小K个数

标签: 数组 分治 快速选择 排序 堆(优先队列)

难度: Medium

设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。

示例:

输入: arr = [1,3,5,7,2,4,6,8], k = 4
输出: [1,2,3,4]

提示:

  • 0 <= len(arr) <= 100000
  • 0 <= k <= min(100000, len(arr))

Submission

运行时间: 35 ms

内存: 21.1 MB

class Solution:
    def smallestK(self, arr: List[int], k: int) -> List[int]:
        if k == 0:
            return []
        self.random_select(arr, 0, len(arr) - 1, k)
        return arr[:k]
    
    def random_select(self, arr, left, right, k):
        pivotIndex = self.partition(arr, left, right)
        nums = pivotIndex - left + 1
        if k < nums:
            self.random_select(arr, left, pivotIndex - 1, k)
        elif k > nums:
            self.random_select(arr, pivotIndex + 1, right, k - nums)

    def partition(self, nums, left, right):
        import random
        p = random.randint(left, right)
        nums[left], nums[p] = nums[p], nums[left]
        le = left + 1
        ge = right
        pivot = nums[left]

        while True:
            while le <= ge and nums[le] < pivot:
                le += 1
            while le <= ge and nums[ge] > pivot:
                ge -= 1
            if le >= ge:
                break
            nums[le], nums[ge] = nums[ge], nums[le]
            le += 1
            ge -= 1
        nums[left], nums[ge] = nums[ge], nums[left]
        return ge

Explain

本题解采用了快速选择算法来找到数组中最小的k个数。快速选择是快速排序算法的变形,主要用于在未完全排序的数组中找到第k小的元素。算法首先选择一个随机的枢轴元素,然后将数组分为小于枢轴和大于枢轴的两部分,类似于快速排序。根据枢轴的位置与k的关系,决定是继续在左侧还是右侧递归查找。如果枢轴位置正好是k,那么左侧的元素就是所需的最小的k个数。

时间复杂度: O(n)

空间复杂度: O(log n)

class Solution:
    def smallestK(self, arr: List[int], k: int) -> List[int]:
        if k == 0:
            return []  # 如果k为0,直接返回空列表
        self.random_select(arr, 0, len(arr) - 1, k)  # 调用递归函数进行快速选择
        return arr[:k]  # 返回前k个元素
    
    def random_select(self, arr, left, right, k):
        pivotIndex = self.partition(arr, left, right)  # 进行一次划分
        nums = pivotIndex - left + 1  # 计算枢轴左侧的元素数量
        if k < nums:
            self.random_select(arr, left, pivotIndex - 1, k)  # 如果k小于枢轴左侧的数量,递归在左侧继续查找
        elif k > nums:
            self.random_select(arr, pivotIndex + 1, right, k - nums)  # 如果k大于枢轴左侧的数量加1,递归在右侧查找

    def partition(self, nums, left, right):
        import random
        p = random.randint(left, right)  # 随机选择一个枢轴
        nums[left], nums[p] = nums[p], nums[left]  # 将枢轴交换到最左侧
        le = left + 1
        ge = right
        pivot = nums[left]  # 枢轴的值

        while True:
            while le <= ge and nums[le] < pivot:
                le += 1  # 移动左指针
            while le <= ge and nums[ge] > pivot:
                ge -= 1  # 移动右指针
            if le >= ge:
                break  # 指针相遇,停止循环
            nums[le], nums[ge] = nums[ge], nums[le]  # 交换左右指针的元素
            le += 1
            ge -= 1
        nums[left], nums[ge] = nums[ge], nums[left]  # 将枢轴交换到最终位置
        return ge  # 返回枢轴的最终位置

Explore

使用随机选取枢轴的主要目的是为了避免在最坏的情况下算法的性能退化。在一些极端情况下,如果总是选择固定位置的元素作为枢轴,例如总是选择第一个或最后一个元素,那么对于已经排序好的数组或者有大量重复元素的数组,快速选择算法会退化成线性时间复杂度,效率大大降低。通过随机选择枢轴,可以在平均情况下保证算法的效率更加稳定。

在`partition`函数中,交换`nums[le]`和`nums[ge]`后,需要移动`le`和`ge`的指针以继续检查后续元素,确保所有小于枢轴的元素都在枢轴的左侧,而大于枢轴的元素都在枢轴的右侧。此外,移动指针也是为了防止重复比较已经交换过的元素,从而避免陷入无限循环,确保每次循环都能进展向前,直到两个指针相遇,结束循环。

在`random_select`函数中,如果`k`正好等于`nums`,即枢轴的位置,表明枢轴左侧的所有元素加上枢轴本身正好是所需的最小的k个数。此时,枢轴左侧的所有元素都已经是小于等于枢轴的值,不需要进一步排序或选择,因此可以直接返回结果,无需再递归进一步处理。这样做可以提高算法的效率,避免不必要的计算。