面试题 17.14. 最小K个数

Posted 炫云云

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试题 17.14. 最小K个数相关的知识,希望对你有一定的参考价值。

面试题 17.14. 最小K个数

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

示例:

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

排序

对原数组从小到大排序后取出前 k k k 个数即可。

class Solution:
    def smallestK(self, arr: List[int], k: int) -> List[int]:
        arr.sort()
        return arr[:k]

复杂度分析

  • 时间复杂度: O ( n log ⁡ n ) O(n \\log n) O(nlogn), 其中 n n n 是数组 arr 的长度。算法的时间复杂度即排序的时间复杂 度。
  • 空间复杂度: O ( log ⁡ n ) O(\\log n) O(logn), 排序所需额外的空间复杂度为 O ( log ⁡ n ) O(\\log n) O(logn)​ 。

大根堆

我们需要k个最小数,记录一个大小为k的大顶堆即可.首先将前 k k k 个数插入大根堆中,随后从第 k + 1 k+1 k+1 个数开始遍历,如果当前遍历到的数比大根堆的堆顶的数要小,就把堆顶的数弹出,再插入当前遍历到的数。最后将大根堆里的数存入数组返回即可。

而 Python 语言中的堆为小根堆,因此我们要对数组中所有的数取其相反数,才能使用小根堆维护前 k k k 小值。

class Solution:
    def smallestK(self, arr: List[int], k: int) -> List[int]:
        ans = []
        for num in arr:
            heapq.heappush(ans, -num)
            if len(ans) > k:   # 大小为k的大根堆
                heapq.heappop(ans) # 删除最大的元素
        return [-num for num in ans]
  • 时间复杂度:O ( n log ⁡ k ) (n \\log k) (nlogk), 其中 n n n 是数组 a r r \\mathrm{arr} arr 的长度。由于大根堆实时维护前 k k k 小值, 所以 插入删除都是 O ( log ⁡ k ) O(\\log k) O(logk) 的时间复杂度,最坏情况下数组里 n n n 个数都会插入,所以一共需要 O ( n log ⁡ k ) O(n \\log k) O(nlogk) 的时间复杂度。
  • 空间复杂度:O ( k ) (k) (k), 因为大根堆里最多 k k k​ 个数。

排序

本题使用排序算法解决最直观,对数组 arr 执行排序,再返回前 k 个元素即可。使用任意排序算法皆可,本文采用并介绍 快速排序 ,为下文 方法二 做铺垫。

快速排序原理:

  • 快速排序算法有两个核心点,分别为 “哨兵划分” 和 “递归” 。
  • 哨兵划分操作: 以数组某个元素(一般选取首元素)为 基准数 ,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。

如下图所示,为哨兵划分操作流程。通过一轮 哨兵划分 ,可将数组排序问题拆分为 两个较短数组的排序问题 (本文称之为左(右)子数组)。

递归:左子数组右子数组 递归执行 哨兵划分,直至子数组长度为 1 时终止递归,即可完成对整个数组的排序。

如下图所示,为示例数组 [2,4,1,0,3,5] 的快速排序流程。观察发现,快速排序和 二分法 的原理类似,都是以 log ⁡ \\log log 时间复杂度实现搜索区间缩小。

class Solution(object):
    def getLeastNumbers(self, arr, k):
        """
        :type arr: List[int]
        :type k: int
        :rtype: List[int]
        """
        def quick_sort(arr, l, r):
            # 子数组长度为 1 时终止递归
            if l >= r: 
                return
            # 哨兵划分操作(以 arr[l] 作为基准数
            i, j = l, r
            while i < j:
                while i <j and arr[j] >= arr[l]:
                    j-=1
                while i <j and  arr[i] <= arr[l]:
                    i +=1
                arr[i], arr[j] = arr[j], arr[i] # arr[j] < arr[l] <arr[i]时候
            arr[l], arr[i] = arr[i], arr[l] # 交换哨兵
            # 递归左(右)子数组执行哨兵划分
            quick_sort(arr,l,i-1)
            quick_sort(arr,i+1,r)

        quick_sort(arr, 0, len(arr)-1)
        return arr[:k]
                

  • 时间复杂度 O ( N log ⁡ N ) O(N \\log N) O(NlogN) : 库函数、快排等排序算法的平均时间复杂度为 O ( N log ⁡ N ) O(N \\log N) O(NlogN)
  • 空间复杂度 O ( N ) O(N) O(N) :快速排序的递归深度最好 (平均) 为 O ( log ⁡ N ) O(\\log N) O(logN), 最差情况 (即输入数 组完全倒序 ) ) ) O ( N ) O(N) O(N)​ 。

快速排序

快排的划分:小于等于分界值 pivot 的元素的都会被放到数组的左边,大于的都会被放到数组的右边,然后返回分界值的下标 left

定义函数 select_k(nums,left,right) 表示划分数组 nums[left,right] 部分,使前k 小的数在数组的左侧,调用快排的划分函数,假设划分函数返回的下标是 pos(表示分界值 pivot 最终在数组中的位置),即 pivot 是数组中第 pos 小的数,那么一共会有三种情况:

  • 如果 pos == k-1,表示 pivot 就是第 k 小的数,直接返回即可;
  • 如果 pos < k-1,表示第 k 小的数在 pivot 的右侧,因此递归调用select_k(nums,pos+1 ,right)
  • 如果 pos > k-1,表示第 k小的数在 pivot 的左侧,递归调用 select_k(nums,left ,pos-1)
class Solution:
    def smallestK(self, nums: List[int], k: int) -> List[int]:
        
        def quick_sort(lists,left,right):
            if left >= right:
                return right
            # 选择参考点,该调整范围的第1个值
            pivot = lists[left]
            low = left  
            high = right
            while left < right:
                # 从右边开始查找大于参考点的值
                while left < right and lists[right] >= pivot:
                    right -= 1
                # 此时 lists[right] <pivot
                lists[left] = lists[right]  # 小于参考点的值先挪到左边 
                while left < right and  lists[left] <= pivot:
                    left += 1
                lists[right] = lists[left]  # 大于参考点的值值挪到右边
                
            # 写回改成的值
            lists[left] = pivot 
            return left 
    
        def select_k(nums,left,right):
            pos = quick_sort(nums,left,right)
            if pos == k-1:
                return #pos =index 
            elif  pos < k-1: # 在 pos 右侧
                select_k(nums,pos+1 ,right)
            else:                          # 在 pos 左侧
                select_k(nums,left ,pos-1) 

        select_k(nums,0, len(nums) - 1)
        return nums[:k]
#快速排序减治
class Solution:
    def smallestK(self, nums: List[int], k: int) -> List[int]:
        def select_k(l, r):
            if l >= r:
                return
            else:
                i = quick_sort(l, r)
                if i == k or i == k - 1:
                    return
                else:
                    if i < k - 1:
                        select_k(i + 1, r)
                    else:
                        select_k(l, i - 1)

        def quick_sort(l, r):
            pivot = nums[l] 
            j = l
            for i in range(l + 1, r + 1):
                if nums[i] < pivot:
                    j += 1
                    nums[i], nums[j] = nums[j], nums[i]
            nums[j], nums[l] = nums[l], nums[j]
            return j
        select_k(0, len(nums) - 1)
        return nums[:k]

基于快速排序的数组划分

题目只要求返回最小的 k 个数,对这 k 个数的顺序并没有要求。因此,只需要将数组划分为 最小的 k 个数其他数字 两部分即可,而快速排序的哨兵划分可完成此目标。

根据快速排序原理,如果某次哨兵划分后 基准数正好是第 k + 1 k+1 k+1​ 小的数字 ,那么此时基准数左边的所有数字便是题目所求的 最小的 k k k 个数 。

根据此思路,考虑在每次哨兵划分后,判断基准数在数组中的索引是否等于 k k k ,若 true 则直接返回此时数组的前 k 个数字即可。

算法流程:

getLeastNumbers() 函数:

  1. k k k 大于数组长度,则直接返回整个数组;
  2. 执行并返回 quick_sort() 即可;

quick_sort() 函数:

注意,此时 quick_sort() 的功能不是排序整个数组,而是搜索并返回最小的 k k k 个数。

  1. 哨兵划分:
  • 划分完毕后,基准数为 arr[i] ,左 / 右子数组区间分别为 [l,i−1] , [i + 1, r]
  1. 递归或返回
  • k < i k < i k<i ,代表第 k + 1 k + 1 k+1​ 小的数字在 左子数组 中,则递归左子数组;
  • k > i k > i k>i ,代表第 k + 1 k + 1 k+1​小的数字在 右子数组 中,则递归右子数组;
  • k = i k = i k=i,代表此时 a r r [ k ] arr[k] arr[k] 即为第 k + 1 k + 1 k+1 小的数字,则直接返回数组前 k k k 个数字即可;

class Solution(object):
    def getLeastNumbers(self, arr, k):
        """
        :type arr: List[int]
        :type k: int
        :rtype: List[int]
        """
        if k >= len(arr): 
            return arr
        def quick_sort(l, r):
            i , j =l,r
            while i <j:
                while i <j and arr[j] >= arr[l]:
                    j-=1
                while i <j and  arr[i] <= arr[l]:
                    i +=1
                arr[i], arr[j] = arr[j], arr[i]
            arr[l], arr[i] = arr[i], arr[l]
            # i 为哨兵的索引
            if k < i: return quick_sort(l, i - 1) 
            if k > i: return quick_sort(i + 1, r)
            return arr[:k]
            
        return quick_sort(0, len(arr) - 1)

参考

Krahets - 力扣(LeetCode) (leetcode-cn.com)

以上是关于面试题 17.14. 最小K个数的主要内容,如果未能解决你的问题,请参考以下文章

面试题 17.14. 最小K个数分治法

面试题 17.14. 最小K个数分治法

Leetcode刷题100天—面试题 17.14. 最小K个数(优先队列)—day27

Leetcode刷题100天—面试题 17.14. 最小K个数(优先队列)—day27

C++&Python描述 LeetCode 面试题 17.14. 最小K个数

LeetCode 面试题 17.14. 最小K个数(堆排,快排)/剑指 Offer 10- I. 斐波那契数列 /470. 用 Rand7() 实现 Rand10()(拒绝采样,学!!!)