由于 Golang 的标准库中包含现成的heap包,所以网上大部分文章都是在写如何使用这个heap包,不过堆排作为大厂的一个常见面试考点,是不会满足于仅让你用heap包去实现的,至少要做到能够手搓一个简易版堆排。
以下是来自力扣官网的友情提醒:「堆排」在很多大公司的面试中都很常见,不了解的同学建议参考《算法导论》或者大家的数据结构教材,一定要学会这个知识点哦!^_^
一个简易版的堆排,主要包含四个方法(方法名称随意):
- 插入-Push:用于向堆中插入数据
- 弹出-Shift:用于取出堆顶数据
- 上浮-Up:一般不对外,仅在堆内部使用,主要用于在插入数据后重构堆
- 下沉-Down:同上,一般仅在内部使用,主要用于在取出堆顶数据后重构堆
由于go标准包集成了小根堆,所以此处实现一个大根堆,如果想要小根堆,只需将上浮下沉中的判断条件取反即可;
废话不多说,直接上代码:
// 大根堆结构:用一个队列模拟二叉树结构
type MaxHeap struct {
Que []int
}
// 插入数据:插到切片末尾,执行上浮操作
func (p *MaxHeap) Push(v int) {
p.Que = append(p.Que, v)
p.HeapUp(len(p.Que) - 1)
}
// 上浮逻辑:较简单,就是不断与parent节点值对比,大于parent,就交换双方的值
func (p *MaxHeap) HeapUp(idx int) {
parentId := (idx - 1) / 2 //获取parent节点下标:由于0下标是堆顶,所以是 (idx - 1) / 2
if parentId < 0 || p.Que[idx] <= p.Que[parentId] {
return
}
p.Que[idx], p.Que[parentId] = p.Que[parentId], p.Que[idx]
p.HeapUp(parentId)
}
// 取出堆顶数据:0下标即为堆顶;为了维持堆结构的正确性,取出后需与最后一个叶子节点交换数据并执行下沉操作
func (p *MaxHeap) Shift() int {
n := len(p.Que)
// 0下标代表堆顶:即堆中的最大或最小值
v := p.Que[0]
// 注意这里:需要将队列的最后一个下标与0下标调换值,再对0下标执行下沉操作;
// 由于是模拟二叉树,所以队列最后一个下标,即代表最后一个叶子节点,之所以与这个节点调换,是因为:
// 堆排是一棵【完全二叉树】,最后一个叶子节点被移除后,不影响完全二叉树结构,正好用来替换被移除的头节点;
p.Que[0], p.Que[n-1] = p.Que[n-1], p.Que[0]
p.Que = p.Que[:n-1]
p.HeapDown(0)
return v
}
// 下沉操作:不断与自己的左右孩子节点对比,并与其中的较大值作调换
func (p *MaxHeap) HeapDown(idx int) {
n := len(p.Que)
// 获取左右孩子下标:堆排中的队列下标其实就是模拟二叉树的层序遍历顺序;
// Root节点是0下标,依次类推,左右孩子坐标不难推导出来;
L, R := idx*2+1, idx*2+2
if n == 0 || L >= n {
return // 越界判断
}
peakId := idx // 峰值下标,即子堆的堆顶
// 分别与左右孩子对比,选择其中的较大值作为子堆的堆顶
if p.Que[peakId] < p.Que[L] {
peakId = L
}
if R < n && p.Que[peakId] < p.Que[R] {
peakId = R
}
if peakId != idx {
// 如果左右孩子的值大于自身,则调换值,继续下沉
p.Que[idx], p.Que[peakId] = p.Que[peakId], p.Que[idx]
p.HeapDown(peakId)
}
}
堆排实践:
这里选取力扣【215. 数组中的第K个最大元素】作为测试题目:
func findKthLargest(nums []int, k int) int {
mHeap := MaxHeap{}
for _, v := range nums {
mHeap.Push(v) // 建立堆
}
for i := 0; i < k-1; i++ {
mHeap.Shift() // 执行 k-1 次堆顶取出操作,则此时的堆顶即为答案
}
return mHeap.Que[0]
}
func main() {
arr := []int{5, 2, 4, 1, 3, 6, 0}
fmt.Println(findKthLargest(arr, 4))
}
// 输出:3
时间复杂度:O(nlogn),建堆的时间代价是 O(n),删除的总代价是 O(klogn),因为 k<n,故渐进时间复杂为 O(n+klogn)=O(nlogn)。
空间复杂度:O(logn),即递归使用栈空间的空间代价。
顺带吐槽一句:力扣的部分题解,纯粹是为了炫技写的,可读性真的不敢恭维。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。