找出数组中前K个最小的数-Python实现

本文介绍两种高效算法来解决寻找数组中第K大元素的问题。一是通过构建二叉堆,实现元素筛选,时间复杂度为O(NLogK)。二是采用快排思想,直接定位目标元素,平均时间复杂度为O(N)。文章提供了详细的算法实现代码及分析。
  寻找数组中给定的第K大的数,或者前K个最大的数,与之同理,稍加改动即可

思路1:二叉堆。

假设数组长度为N,首先取前K个数,构建二叉堆(大顶堆),然后将剩余N-K个元素,依次与堆顶元素进行比较,若小于堆顶元素,则替换, 并重新为大顶堆。

代码如下
# 最大堆下沉调整,始终保持最大堆
def downAdjust(ary_list, parent_index, length):
    tmp = ary_list[parent_index]
    child_index = 2 * parent_index + 1

    while child_index < length:
        if child_index + 1 < length and ary_list[child_index + 1] > ary_list[child_index]:
            child_index += 1

        if tmp >= ary_list[child_index]:
            break

        ary_list[parent_index] = ary_list[child_index]
        parent_index = child_index
        child_index = 2 * parent_index + 1

    ary_list[parent_index] = tmp
    pass

# 构建堆
def build_heap(ary_list, k):
    index = k // 2 - 1  # 最后一个非叶子结点
    while index >= 0:
        downAdjust(ary_list, index, k)
        index -= 1
    pass


# 利用最大堆找出前K个最小值
# 每次从原数组中拿出一个元素和当前堆顶值比较,
# 然后判断是否可以放入,放入后继续调整堆结构
def heapK(ary, nums, k):
    if nums <= k:
        return nums
        
    ks = ary[:k]
    build_heap(ks, k)           # 构建大顶堆(先不排序)
    # print('build heap:', ks)
    
    for index in range(k, nums):
        ele = ary[index]
        if ks[0] > ele:
            ks[0] = ele
            downAdjust(ks, 0, k)
            # print('heap adjust:', ks)

    # 如果需要则输出排序结果
    # heap_sort(ks)
    return ks
    pass


if __name__ == '__main__':

    # *** 测试方法1
    ary_list = [10, 2, 38, 9, 22, 53, 47, 7, 3, 97]
    nums = len(ary_list)
    print('{} original data:'.format(nums), ary_list)

    # # 原始数组的排列顺序(作为ks的对比)
    # build_heap(ary_list, nums)
    # heap_sort(ary_list)
    # print('{} original sorted data:'.format(nums), ary_list)

    for k in range(6, nums + 1):
        ks = heapK(ary_list, nums, k)
        print('{}th data:'.format(k), ks)
        break
    pass
结果如下图所示:

在这里插入图片描述

如果想要输出结果有序,则可以增加如下方法,进行堆排序即可。
# 堆排序(最大堆)
def heap_sort(ary):
    length = len(ary)

    index = length - 1
    # 依次移除堆顶元素(放入末尾),并将末尾元素放在堆顶,进行下沉调整,
    # 使得每次都会有非最大值上浮到堆顶,并重新调整为大顶堆;
    # 然后再重复上述操作。
    while index >= 0:
        tmp = ary[0]
        ary[0] = ary[index]
        ary[index] = tmp
        downAdjust(ary, 0, index)
        index -= 1

    pass

总结

针对二叉堆的思路,其实主要是寻找能容纳K个元素的容器,然后在该容器中进行筛选操作。
若想要输出有序结果,则可以选择不同的排序算法对K个元素进行排序即可。

时间复杂度
  1. 构建大顶堆 :平均复杂度为O(KLogK),;
  2. 元素筛选 :剩余N-K个元素,最坏情况下每个元素都要进行堆调整,复杂度为O((N-K)LogK);

所以总的平均时间复杂度为O(KLogK)+O((N-K)LogK)=O(NLogK)。

空间复杂度为O(K)


思路2:快排思想。

利用快排的思想,循环找到第K个位置安放正确的元素,此时K的左边是小于K位置元素的元素,右边是大于K位置元素的元素,即前K个元素就是问题答案。

代码如下:
# 类似于快排的思想,不同的地方在于每趟只需要往一个方向走
# 按照从小到大的顺序,寻找前K个最小值
def qselect(ary_list, k):
    if len(ary_list) < k:
        return ary_list

    tmp = ary_list[0]
    left = [x for x in ary_list[1:] if x <= tmp] + [tmp]
    llen = len(left)
    if llen == k:
        return left
    if llen > k:
        return qselect(left, k)
    else:
        right = [x for x in ary_list[1:] if x > tmp]
        return left + qselect(right, k-llen)
    pass

结果如下

在这里插入图片描述

时间复杂度:最坏情况下,每次只能找出一个最小值,总共需要找K次,复杂度为O(KN);最好情况下只需要遍历依次即可找到,复杂度为O(N);当K远小于N时,平均时间复杂度为O(N)。
空间复杂度:因为开辟了存储待寻找元素的数组,所以空间复杂度为O(N)。

总结
关于此类型问题,目前所总结和学习到的以这两种方案为主,仅作分享。
若有错漏,欢迎交流指正。

参考链接:
(1)漫画:什么是堆排序?.
(2)最大K个数问题的Python版解法总结

感谢~

<think>好的,我现在需要解决如何高效地找出数组中的最小k个元素的问题。首先,我应该回顾一下已有的知识,看看有哪些算法数据结构适合这个问题。 用户提到了堆数据结构,引用[1]中说到可以用大小为k的最大堆来辅助找到最小的k个。这个方法应该是可行的,因为最大堆的堆顶元素是当堆中最大的,每次新元素进来时,如果比堆顶小,就替换掉堆顶,然后调整堆,这样最后堆里保存的就是最小的k个元素。这个思路的时间复杂度应该是O(n log k),因为每次插入和删除堆顶的操作都是O(log k),而需要遍历n个元素。 另外,引用[3]提到了一种基于快速选择的方法,平均时间复杂度是O(n),但最坏情况下是O(n²)。不过可以通过随机选择枢轴或者使用中位来优化,使得最坏情况出现的概率降低。快速选择算法通常用于找出第k小的元素,一旦找到第k小的元素,那么它左边的元素都是比它小的,这样就可以直接得到最小的k个。不过需要注意处理重复元素的情况,以及是否需要保持这些元素的顺序。 那么现在问题是如何在编程中实现这两种方法,并比较它们的优缺点。用户可能关心的是哪种方法在什么情况下更高效。比如,当k远小于n时,堆方法可能更优,因为log k的影响较小;而当k接近n时,快速选择可能更好,因为平均情况是线性的时间复杂度。 另外,还要考虑实现上的复杂度。堆结构需要维护一个堆,可能需要自己实现或者使用优先队列。而快速选择则需要实现分区操作,类似于快速排序的分区步骤,这在代码上可能稍微复杂一些,尤其是处理边界条件和随机选择枢轴的部分。 接下来,我需要具体思考如何用代码实现这两种方法。比如在Python中,可以使用heapq模块来实现堆方法,因为heapq默认是最小堆,而我们需要最大堆,可以通过存储元素的相反实现。或者直接使用heapq的nlargest函,但可能不符合题目的要求,需要手动实现。 对于快速选择方法,需要实现一个分区函,随机选择一个枢轴,将数组分成小于枢轴和大于等于枢轴的两部分,然后根据第k小的位置决定递归处理哪一部分。当枢轴的位置正好是k-1时,就找到了结果。 另外,还需要考虑是否需要修改原数组,或者是否允许改变元素的顺序。如果允许修改原数组,快速选择可能更高效;否则可能需要额外的空间。 然后,需要验证这些方法的正确性,比如测试不同的输入情况,包括k=1,k=n,数组中有重复元素等情况。 最后,还需要考虑其他可能的优化方法,比如当据量特别大时,无法全部载入内存,可能需要外部排序或者分块处理,但这可能超出了当问题的范围。 总结下来,主要的两种方法是基于堆和基于快速选择,各有优缺点,需要根据具体情况选择。</think>### 如何高效找出数组中的最小k个元素? #### 方法1:基于最大堆的算法(时间复杂度 $O(n \log k)$) 1. **数据结构选择** 使用大小为 $k$ 的最大堆(大顶堆),堆顶始终保存当堆中最大元素。遍历数组时,若元素小于堆顶则替换堆顶并调整堆结构。 2. **Python实现** ```python import heapq def find_smallest_k_heap(arr, k): if k <= 0: return [] max_heap = [] for num in arr: if len(max_heap) < k: heapq.heappush(max_heap, -num) # 存储相反以模拟最大堆 else: if -num > max_heap[0]: heapq.heappop(max_heap) heapq.heappush(max_heap, -num) return sorted([-x for x in max_heap]) ``` 3. **分析** - 适合处理据流或动态据。 - 空间复杂度为 $O(k)$,适合 $k \ll n$ 的场景[^1]。 --- #### 方法2:基于快速选择的算法(平均时间复杂度 $O(n)$,最坏 $O(n^2)$) 1. **算法原理** 基于快速排序的分区思想,随机选择枢轴将数组分为两部分,递归处理包含第 $k$ 小元素的子数组。 2. **Python实现** ```python import random def quick_select(arr, k): pivot = random.choice(arr) left = [x for x in arr if x < pivot] mid = [x for x in arr if x == pivot] right = [x for x in arr if x > pivot] if k <= len(left): return quick_select(left, k) elif k <= len(left) + len(mid): return pivot else: return quick_select(right, k - len(left) - len(mid)) def find_smallest_k_quick(arr, k): if k <= 0: return [] kth_smallest = quick_select(arr, k) result = [x for x in arr if x < kth_smallest] if len(result) < k: result += [kth_smallest] * (k - len(result)) return sorted(result) ``` 3. **分析** - 平均时间复杂度为 $O(n)$,但需注意最坏情况可通过随机化枢轴优化[^3]。 - 可能修改原数组顺序,适合允许原地操作的场景。 --- #### 方法对比 | 方法 | 时间复杂度 | 空间复杂度 | 适用场景 | |--------------|------------------|------------|------------------------------| | 最大堆 | $O(n \log k)$ | $O(k)$ | 据流、$k \ll n$ | | 快速选择 | 平均 $O(n)$ | $O(n)$ | 允许修改数组、要求高效 | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值