Python语言中的堆为最小堆,所以用Python刷题都是在最小堆的基础上。
先说一个结论:最大堆解决取最小的问题,最小堆解决取最大的问题
4.22 找最小的k个数
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
例如
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
根据之前的结论,取最小用大根堆。那具体的思路是:
维护长度为k的大根堆,新的数和堆顶元素比较,如果小于堆顶,则堆顶出来,新的元素进去,这样的k个元素就是最小的。
其实和堆顶元素的比较,就是和当前第k小的元素(守门员)比较的过程,你赢了你就有资格进去,守门员爬。
而Python的堆是小根堆,所以解题的时候需要取相反数,问题转化为用小根堆找相反数arr中最大的k个值。
代码如下:
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
if k == 0:
return list()
#初始化长度为k的堆
hp = [-x for x in arr[:k]]
heapq.heapify(hp)
for i in range(k, len(arr)):
if -hp[0] > arr[i]:
heapq.heappop(hp)
heapq.heappush(hp, -arr[i])
ans = [-x for x in hp]
return ans
6.29 合并升序链表
关于堆中复杂元素的比较问题,默认是取第一个元素,详情见:
Python heapq 自定义比较器
给你一个链表数组(不知道多少个),每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
例如:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
这道题是困难,但只是麻烦。有点像那种综合题,需要用到多个知识点。
先来第一种解法,用到了排列两个链表数组 + 归并的解。就是把问题拆分嘛,排列多个我不会,但是排列两个我会,这里注意一下归并排序的函数,有点难想- -
def merge(lists: List[Optional[ListNode]], l, r):
if (l == r):
return lists[l]
if (l > r):
return None
else:
mid = (l + r) // 2
return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r))
第二种就利用到了小根堆,每次输出最小的一个元素。输出完再补元素进堆(如果对应链表有的话),注意一个问题,链表结点不能直接进小根堆,得变成元祖(val,id)进去,这样默认比的是val。根据id数判断是哪一个链表的结点。
class Solution:
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
hp, K, res = [], len(lists), ListNode()
head = res
for i in range(K):
if (lists[i]):
heapq.heappush(hp, (lists[i].val, i))
while (hp):
val, idx = heapq.heappop(hp)
head.next = ListNode(val)
head = head.next
lists[idx] = lists[idx].next
if(lists[idx]):
heapq.heappush(hp, (lists[idx].val, idx)) #如果lists[idx]后面还有元素,后面的元素继续进堆!
return res.next
求第k个最小的子序列和
首先考虑本题的简化问题:给定 n 个非负数 a 1 , a 2 , ⋯ , a n a_1, a_2, \cdots, a_n a1,a2,⋯,an ,求第 k 个最小的子序列和。
我们先把所有数从小到大排序,记 (s, i) 表示一个总和为 s,以第 i 个元素结尾的子序列。
用一个小根堆维护
(
s
,
i
)
(s, i)
(s,i),一开始堆中只有一个元素
(
a
1
,
1
)
(a_1, 1)
(a1,1)
当我们取出堆顶元素 (s, i)(s,i) 时,我们可以进行以下操作:
①把
a
i
+
1
a_{i + 1}
ai+1接到这个子序列的后面形成新的子序列。
②把子序列中的
a
i
a_i
ai 直接换成
a
i
+
1
a_{i + 1}
ai+1
为什么只换最后一个?因为最后一个是【比较大的,换成大的】不会跳过别的子序列。
第 (k - 1)次取出的 (s, i)中的 s 就是答案(k = 1 时答案为空集之和,也就是 0)。
这个做法的正确性基于以下事实:
这种方法能不重不漏地生成所有子序列。
每次放进去的数不小于拿出来的数。
可以动笔试一下[1,2,4,8],就能大概理解了。