目录
1、方式一:通过自定义实现建堆和堆化操作
2、方式二:借助模块heapq实现
2.1、模块heapq的基本使用
2.2、使用heapq实现最小的k个数
3、堆在实际项目的应用
实现语言:Python 3.9
题目来源:牛客
分析:
- 要找到最小的k个元素,只需要准备k个数字,之后每次遇到一个数字能够快速的与这k个数字中最大的值比较(大顶堆),每次将最大的值替换掉,那么最后剩余的就是k个最小的数字了。
为什么不是与k个元素中的最小值(小顶堆)进行比较呢?
- 我们使用示例分析为什么使用小顶堆就不能实现,对于[4, 5, 1, 6, 2, 7, 3, 8] k = 4 建立小顶堆,建立的小顶堆为[1, 5, 4, 6] 堆顶元素为1,此时使用元素2和堆顶元素比较,比堆顶元素大,不加入堆顶;那么这样可见最后得到的结果一定不是最小的k个元素,所以不能使用小顶堆。
实现步骤:
- 1:利用input数组中前k个元素,构建一个大小为k的大顶堆,堆顶为这k个元素的最大值。
- 2:对于后续的元素,依次比较其与堆顶的大小,若是比堆顶小,则堆顶弹出,再将新数加入堆中,直至数组结束,保证堆中的k个最小。
- 3:最后将堆顶依次弹出即是最小的k个数。
实现方式:
- 方式一:通过自定义实现建堆和堆化操作
- 方式二:借助Python模块heapq实现
1、方式一:通过自定义实现建堆和堆化操作
class Solution:
def GetLeastNumbers_Solution(self , input: List[int], k: int) -> List[int]:
# write code here
# 建堆(从最后一个非叶子节点到根节点建堆)
def buildHeap(a, n):
i = n//2 - 1
while i >=0:
heapify(a, n, i)
i = i - 1
# 堆化(从上到下)
def heapify(a, n, i):
while True:
maxPos = i
# 和左子节点比较
if i*2 + 1 <= n and a[i] < a[i*2+1]:
maxPos = i*2 + 1
# 和右子节点比较
if i*2 + 2 <= n and a[maxPos] < a[i*2+2]:
maxPos = i*2 + 2
if maxPos == i:
break
# 交换节点
a[i], a[maxPos] = a[maxPos], a[i]
i = maxPos
# 特殊情况处理
if len(input) == 0 or len(input) <= k:
return input
# 先前k个数据建堆
buildHeap(input, k)
# 后面的节点依次和堆顶元素比较
# 如果比堆顶元素小,将该元素加入堆顶,然后从堆顶开始堆化
for i in range(k, len(input)):
if input[0] > input[i]:
# 该元素和堆顶元素交换
input[0], input[i] = input[i], input[0]
# 从堆顶元素开始从上往下堆化
heapify(input, k, 0)
return input[:k]
2、方式二:借助模块heapq实现
- 注意:heapq模块默认建立的是小顶堆,所以需要借助这个特性自定义实现大顶堆
2.1、模块heapq的基本使用
import heapq
# 创建一个空堆
heap = []
# 往堆中添加元素
heapq.heappush(heap, 3)
heapq.heappush(heap, 1)
heapq.heappush(heap, 4)
heapq.heappush(heap, 1)
heapq.heappush(heap, 5)
# 弹出最小元素
print(heapq.heappop(heap)) # 输出 1
print(heapq.heappop(heap)) # 输出 1
# 弹出最小元素并加入新元素
print(heapq.heapreplace(heap, 2)) # 输出 2
# 将列表转化为堆
heap = [5, 8, 3, 0, 2]
heapq.heapify(heap)
print(heap) # 输出 [0, 2, 3, 8, 5]
# 获取最大的两个元素
print(heapq.nlargest(2, heap)) # 输出 [8, 5]
# 获取最小的两个元素
print(heapq.nsmallest(2, heap)) # 输出 [0, 2]
2.2、使用heapq实现最小的k个数
import heapq
class Solution:
def GetLeastNumbers_Solution(self , input: List[int], k: int) -> List[int]:
# 特殊情况处理
if len(input) == 0 or len(input) < k or k == 0:
return []
# 自定义实现建立大顶堆
def my_max_heapify(iterable):
# 原理是对输入数据取反,这样建立的小顶堆,取数的时候进行取反即是大顶堆
max_heap = [(-x, x) for x in iterable]
# 建堆
heapq.heapify(max_heap)
# 返回max_heap中元祖数据的第二个,忽略第一个数据
return [x for (_, x) in max_heap]
# 前k个数据建堆
max_heap = my_max_heapify(input[:k])
# 后面的数据依次与堆顶元素比较
for i in range(k, len(input)):
if max_heap[0] > input[i]:
# 弹出堆顶元素,加入新元素
heapq.heapreplace(max_heap, input[i])
# 再次对数据进行堆化
max_heap = my_max_heapify(max_heap)
return max_heap
3、堆在实际项目的应用
- 优先级队列:堆常常被用来实现优先级队列。在优先级队列中,元素被赋予一个优先级,并且总是按照优先级的顺序被处理。最大堆或最小堆可以根据需要用来实现具有最高或最低优先级元素始终位于队列头部的优先级队列。这种数据结构在任务调度、网络路由和事件处理等场景中非常有用。
- 堆排序:堆排序是一种基于堆的高效排序算法,其时间复杂度为O(nlogn)。它通过构建最大堆或最小堆,然后不断地取出堆顶元素(最大或最小值)并将其与堆的最后一个元素交换,然后重新调整堆的结构,直到整个数组有序。堆排序在处理大数据集时非常有效。
- 数据流中的Top K问题:在数据流处理中,经常需要找到数据流中最大的K个元素或最小的K个元素。这可以通过使用最大堆或最小堆来实现。具体做法是将数据流中的元素逐个插入堆中,并保持堆的大小为K。如果新插入的元素比堆顶元素(最大或最小)更大(或更小),则删除堆顶元素并插入新元素。这样,堆中始终保存着数据流中最大的K个元素或最小的K个元素。
- 合并有序文件:当需要合并多个有序文件为一个有序文件时,可以使用堆来实现。具体做法是将每个文件的第一个元素放入一个最小堆中,然后每次从堆中取出最小的元素并写入结果文件,然后从该元素所在的文件中取出下一个元素并重新放入堆中。重复这个过程直到所有文件都被处理完毕。
- 高性能定时器:在高性能定时器中,可以使用最小堆来管理定时任务。具体做法是将每个定时任务按照其触发时间放入最小堆中,然后每次从堆中取出触发时间最小的任务并执行。这样可以保证定时任务按照其触发时间的顺序被依次执行。
- 求解中位数:利用最大堆和最小堆,可以在O(logn)的时间内查询一组数据的中位数。具体做法是将数据分为两部分,一部分放入最大堆,另一部分放入最小堆,并保持最大堆中的元素个数不大于最小堆中的元素个数加1。这样,最大堆的堆顶元素(最大值)和最小堆的堆顶元素(最小值)的平均值就是当前数据的中位数。当插入新元素时,根据新元素与当前中位数的大小关系,将其放入最大堆或最小堆中,并重新调整两个堆的大小关系以保持平衡。