第五篇:堆
堆是一种特殊的完全二叉树,特殊之处在于其每个结点的值要么都大于等于两个子结点的值,要么都小于等于两个子结点的值,前者称为大顶堆(堆顶值最大,pop出来的越来越小),后者称为小顶堆(堆顶值最小,pop出来的越来越大)。堆的pop是把堆顶pop出来。
在golang中,实现一个堆需要实现heap.Interface接口,重写该接口的5个方法,Push()、Pop()以及从sort.Interface接口提升的Len()、Less()、Swap()方法。堆底层用切片存储元素,Push()的实现逻辑就是把新元素放到切片中,Pop()的实现逻辑就是把切片的最后一个元素踢出并返回,Len()的实现逻辑就是计算切片的长度,Less()的实现逻辑就是比较切片中元素的大小,Swap()的实现逻辑就是调换切片中两个元素的位置。
如果堆中放的是整数、浮点数或者字符串,那么我们可以偷下懒,在自定义类中引入一个匿名字段,字段类型分别对应是sort.IntSlice、sort.Float64Slice、sort.StringSlice。因为这三个类都实现了sort.Interface接口,自定义类引入它们作为匿名字段的话,就可以不再必须要重写Len()、Less()、Swap()方法了。如果这些类的实现逻辑不满足我们的需求,或者我们想在堆中放自定义的struct,那么就得自己实现了。比如,sort.IntSlice的Less()方法是递增的,这样实现的堆是小顶堆,如果我们想要大顶堆的话,就得自己重写Less()方法了。示例如下:
type IntHeap struct {
sort.IntSlice
}
func (h *IntHeap) Push(a any) {
h.IntSlice = append(h.IntSlice, a.(int))
}
func (h *IntHeap) Pop() any {
arr := h.IntSlice
a := arr[len(arr)-1]
h.IntSlice = arr[:len(arr)-1]
return a
}
这样就可以通过调用heap.Init()函数来初始化堆(即把数组或切片堆化),调用heap.Push()函数往堆中添加一个元素(把元素放到Len()位置,会自动重建堆),调用heap.Pop()函数弹出堆顶(把Len()-1位置的元素移出并返回,会自动重建堆)。注意是调用heap.Push()、heap.Pop()函数,而不是调用我们重写的Push()、Pop()方法,heap.Init()、heap.Push()、heap.Pop()函数是heap包的三个函数,内部会调用我们重写的Len()、Less()、Swap()、Push()、Pop()方法。
堆排序的话,把所有元素通过调用heap.Push()函数放进堆后,再通过heap.Pop()函数弹出,放到一个数组中,这个数组就是有序的。
力扣1046、最后一块石头的重量。easy
力扣703、数组中的第K个最大元素。easy
堆中的元素用数组表示的话,并不是单调递增或递减的,即使元素没有重复。所以,【先通过heap.push()函数把元素逐个放到堆中,然后返回索引k-1处元素】的做法,是不对的。正确做法是:求第K大,就构建一个小顶堆,只保留K个数(push后,如果元素个数超出K,就pop),第K大是堆顶。时间复杂度是O(nlogk)
力扣205、数组中的第K个最大元素。medium
时间复杂度要求为O(n),只能通过改进快排算法来解。否则,按照703题思路,时间复杂度是O(nlogk)。
力扣347、前K个高频元素。medium
遍历数组,把各元素的出现频次用map保存。遍历map,把k-v映射关系(自定义一个struct表示k-v映射)存放到小顶堆中,只保留K个元素,这k个元素的键就是结果集。只保留K个元素有两种方法:第一种,一股脑的把全部键值对丢到堆中,然后丢弃len(m)-k次堆顶。第二种,每次把键值对放到堆中后,看堆中元素个数是否超过了k,如果超过了,就把栈顶丢弃。这样,最终堆中元素个数也会为k。
力扣373、查找和最小的K对数字。medium
很明显,比nums1[i]+nums2[j]大的下一个数对,只可能是nums1[i+1]+nums2[j]或nums1[i]+nums2[j+1]。所以,我们可以构建一个元素是自定义三元组(自定义struct,有三个属性a、b、sum,其中a表示nums1的索引、b表示nums2的索引,sum表示nums1[a]与nums2[b]的和)的小顶堆,先把nums1[0]+nums2[0]入堆,然后弹出,保存到结果集中,并把弹出的元素的相邻的两个元素push到堆中,再次弹出。。。直到结果集的长度为K或者堆中没有元素。有一个问题是,(i, j)可能被push两次(用(i, j)表示nums1的索引是i,nums2的索引是j的元素),弹出的元素是(i-1, j)或者(i, j-1)时,都会push,所以我们得用一个map记录push的元素,如果已经push过,就不再push了。
可以做优化,把map省略掉,在弹出(i, j)时,只push (i, j+1)。但是这样子,只会push (0, 0)、(0, 1)。。。(0, n-1)、(0, n)了(n是nums2的最大索引),所以,一开始不能只push (0, 0)了,应该把(0, 0)、(1, 0)。。。(m-1, 0)、(m, 0)都push进去(m是nums1的最大索引)。考虑一个m×n的矩阵, 每一行的数从左往右依次递增,每一列的数从上往下依次递增。如何找出前K小的数字。先把矩阵第一列的元素都push进堆中,然后弹出堆顶,假如弹出的元素是第i行、第i列的元素(i、j是索引,用(i, j)表示此元素),那么把此元素保存到结果集中,再把(i, j+1)push进去,再弹出栈顶。。。直到结果集的长度为K或者堆中没有元素。
浙公网安备 33010602011771号